algoliasearch - Version 1.5.5

Version Notes

- NEW: Add an option to include data from out-of-stock sub products
- NEW: Use secured api keys to only retrieve one group price in the frontend
- NEW: Better update strategy to simplify the indexer code and to avoid missing deleted products event
- UPDATE: Better handling of include in navigation config
- UPDATE: underlying php client
- UPDATE: Conditionally render template directives
- UPDATE: Make sub product skus searchable
- FIX: slaves creation issue
- FIX: small price issue
- FIX: fallback to default search in case there is a error from the api

Download this release

Release Info

Developer Algolia Team
Extension algoliasearch
Version 1.5.5
Comparing to
See all releases


Code changes from version 1.5.4 to 1.5.5

Files changed (26) hide show
  1. app/code/community/Algolia/Algoliasearch/Helper/Algoliahelper.php +8 -0
  2. app/code/community/Algolia/Algoliasearch/Helper/Config.php +52 -4
  3. app/code/community/Algolia/Algoliasearch/Helper/Data.php +3 -1
  4. app/code/community/Algolia/Algoliasearch/Helper/Entity/Additionalsectionshelper.php +7 -4
  5. app/code/community/Algolia/Algoliasearch/Helper/Entity/Categoryhelper.php +14 -12
  6. app/code/community/Algolia/Algoliasearch/Helper/Entity/Helper.php +2 -1
  7. app/code/community/Algolia/Algoliasearch/Helper/Entity/Pagehelper.php +8 -3
  8. app/code/community/Algolia/Algoliasearch/Helper/Entity/Producthelper.php +61 -53
  9. app/code/community/Algolia/Algoliasearch/Helper/Entity/Suggestionhelper.php +42 -16
  10. app/code/community/Algolia/Algoliasearch/Helper/Logger.php +2 -2
  11. app/code/community/Algolia/Algoliasearch/Model/Indexer/Algolia.php +26 -145
  12. app/code/community/Algolia/Algoliasearch/Model/Indexer/Algoliacategories.php +1 -1
  13. app/code/community/Algolia/Algoliasearch/Model/Resource/Fulltext/Collection.php +13 -1
  14. app/code/community/Algolia/Algoliasearch/etc/config.xml +5 -2
  15. app/code/community/Algolia/Algoliasearch/etc/system.xml +49 -7
  16. app/design/frontend/base/default/template/algoliasearch/beforetopsearch.phtml +11 -1
  17. app/design/frontend/base/default/template/algoliasearch/topsearch.phtml +2 -1
  18. app/etc/modules/Algolia_Algoliasearch.xml +1 -1
  19. lib/AlgoliaSearch/AlgoliaException.php +3 -1
  20. lib/AlgoliaSearch/Client.php +759 -255
  21. lib/AlgoliaSearch/ClientContext.php +164 -23
  22. lib/AlgoliaSearch/Index.php +848 -371
  23. lib/AlgoliaSearch/IndexBrowser.php +160 -0
  24. lib/AlgoliaSearch/PlacesIndex.php +81 -0
  25. lib/AlgoliaSearch/Version.php +4 -2
  26. package.xml +14 -8
app/code/community/Algolia/Algoliasearch/Helper/Algoliahelper.php CHANGED
@@ -7,10 +7,13 @@ if (class_exists('AlgoliaSearch\Client', false) == false)
7
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/ClientContext.php';
8
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Client.php';
9
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Index.php';
 
 
10
  }
11
 
12
  class Algolia_Algoliasearch_Helper_Algoliahelper extends Mage_Core_Helper_Abstract
13
  {
 
14
  protected $client;
15
  protected $config;
16
 
@@ -26,6 +29,11 @@ class Algolia_Algoliasearch_Helper_Algoliahelper extends Mage_Core_Helper_Abstra
26
  $this->client = new \AlgoliaSearch\Client($this->config->getApplicationID(), $this->config->getAPIKey());
27
  }
28
 
 
 
 
 
 
29
  public function getIndex($name)
30
  {
31
  return $this->client->initIndex($name);
7
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/ClientContext.php';
8
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Client.php';
9
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Index.php';
10
+ require_once Mage::getBaseDir('lib').'/AlgoliaSearch/PlacesIndex.php';
11
+ require_once Mage::getBaseDir('lib').'/AlgoliaSearch/IndexBrowser.php';
12
  }
13
 
14
  class Algolia_Algoliasearch_Helper_Algoliahelper extends Mage_Core_Helper_Abstract
15
  {
16
+ /** @var \AlgoliaSearch\Client */
17
  protected $client;
18
  protected $config;
19
 
29
  $this->client = new \AlgoliaSearch\Client($this->config->getApplicationID(), $this->config->getAPIKey());
30
  }
31
 
32
+ public function generateSearchSecuredApiKey($key, $params = array())
33
+ {
34
+ return $this->client->generateSecuredApiKey($key, $params);
35
+ }
36
+
37
  public function getIndex($name)
38
  {
39
  return $this->client->initIndex($name);
app/code/community/Algolia/Algoliasearch/Helper/Config.php CHANGED
@@ -28,16 +28,19 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract
28
  const EXCLUDED_PAGES = 'algoliasearch/autocomplete/excluded_pages';
29
  const MIN_POPULARITY = 'algoliasearch/autocomplete/min_popularity';
30
  const MIN_NUMBER_OF_RESULTS = 'algoliasearch/autocomplete/min_number_of_results';
 
31
 
32
  const NUMBER_OF_PRODUCT_RESULTS = 'algoliasearch/products/number_product_results';
33
  const PRODUCT_ATTRIBUTES = 'algoliasearch/products/product_additional_attributes';
34
  const PRODUCT_CUSTOM_RANKING = 'algoliasearch/products/custom_ranking_product_attributes';
35
  const RESULTS_LIMIT = 'algoliasearch/products/results_limit';
36
  const SHOW_SUGGESTIONS_NO_RESULTS = 'algoliasearch/products/show_suggestions_on_no_result_page';
 
37
 
38
  const CATEGORY_ATTRIBUTES = 'algoliasearch/categories/category_additional_attributes2';
39
  const INDEX_PRODUCT_COUNT = 'algoliasearch/categories/index_product_count';
40
  const CATEGORY_CUSTOM_RANKING = 'algoliasearch/categories/custom_ranking_category_attributes';
 
41
 
42
 
43
  const IS_ACTIVE = 'algoliasearch/queue/active';
@@ -60,6 +63,16 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract
60
 
61
  protected $_productTypeMap = array();
62
 
 
 
 
 
 
 
 
 
 
 
63
  public function isDefaultSelector($storeId = null)
64
  {
65
  return '.algolia-search-input' === $this->getAutocompleteSelector($storeId);
@@ -259,6 +272,11 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract
259
  return array();
260
  }
261
 
 
 
 
 
 
262
  public function getSortingIndices($storeId = NULL)
263
  {
264
  $product_helper = Mage::helper('algoliasearch/entity_producthelper');
@@ -297,22 +315,52 @@ class Algolia_Algoliasearch_Helper_Config extends Mage_Core_Helper_Abstract
297
 
298
  public function getApplicationID($storeId = NULL)
299
  {
300
- return Mage::getStoreConfig(self::APPLICATION_ID, $storeId);
301
  }
302
 
303
  public function getAPIKey($storeId = NULL)
304
  {
305
- return Mage::getStoreConfig(self::API_KEY, $storeId);
306
  }
307
 
308
  public function getSearchOnlyAPIKey($storeId = NULL)
309
  {
310
- return Mage::getStoreConfig(self::SEARCH_ONLY_API_KEY, $storeId);
311
  }
312
 
313
  public function getIndexPrefix($storeId = NULL)
314
  {
315
- return Mage::getStoreConfig(self::INDEX_PREFIX, $storeId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
 
318
  public function getCategoryAdditionalAttributes($storeId = NULL)
28
  const EXCLUDED_PAGES = 'algoliasearch/autocomplete/excluded_pages';
29
  const MIN_POPULARITY = 'algoliasearch/autocomplete/min_popularity';
30
  const MIN_NUMBER_OF_RESULTS = 'algoliasearch/autocomplete/min_number_of_results';
31
+ const RENDER_TEMPLATE_DIRECTIVES = 'algoliasearch/autocomplete/render_template_directives';
32
 
33
  const NUMBER_OF_PRODUCT_RESULTS = 'algoliasearch/products/number_product_results';
34
  const PRODUCT_ATTRIBUTES = 'algoliasearch/products/product_additional_attributes';
35
  const PRODUCT_CUSTOM_RANKING = 'algoliasearch/products/custom_ranking_product_attributes';
36
  const RESULTS_LIMIT = 'algoliasearch/products/results_limit';
37
  const SHOW_SUGGESTIONS_NO_RESULTS = 'algoliasearch/products/show_suggestions_on_no_result_page';
38
+ const INDEX_OUT_OF_STOCK_OPTIONS = 'algoliasearch/products/index_out_of_stock_options';
39
 
40
  const CATEGORY_ATTRIBUTES = 'algoliasearch/categories/category_additional_attributes2';
41
  const INDEX_PRODUCT_COUNT = 'algoliasearch/categories/index_product_count';
42
  const CATEGORY_CUSTOM_RANKING = 'algoliasearch/categories/custom_ranking_category_attributes';
43
+ const SHOW_CATS_NOT_INCLUDED_IN_NAVIGATION = 'algoliasearch/categories/show_cats_not_included_in_navigation';
44
 
45
 
46
  const IS_ACTIVE = 'algoliasearch/queue/active';
63
 
64
  protected $_productTypeMap = array();
65
 
66
+ public function indexOutOfStockOptions($storeId = null)
67
+ {
68
+ return Mage::getStoreConfigFlag(self::INDEX_OUT_OF_STOCK_OPTIONS, $storeId);
69
+ }
70
+
71
+ public function showCatsNotIncludedInNavigation($storeId = null)
72
+ {
73
+ return Mage::getStoreConfigFlag(self::SHOW_CATS_NOT_INCLUDED_IN_NAVIGATION, $storeId);
74
+ }
75
+
76
  public function isDefaultSelector($storeId = null)
77
  {
78
  return '.algolia-search-input' === $this->getAutocompleteSelector($storeId);
272
  return array();
273
  }
274
 
275
+ public function getRenderTemplateDirectives($storeId = NULL)
276
+ {
277
+ return Mage::getStoreConfigFlag(self::RENDER_TEMPLATE_DIRECTIVES, $storeId);
278
+ }
279
+
280
  public function getSortingIndices($storeId = NULL)
281
  {
282
  $product_helper = Mage::helper('algoliasearch/entity_producthelper');
315
 
316
  public function getApplicationID($storeId = NULL)
317
  {
318
+ return trim(Mage::getStoreConfig(self::APPLICATION_ID, $storeId));
319
  }
320
 
321
  public function getAPIKey($storeId = NULL)
322
  {
323
+ return trim(Mage::getStoreConfig(self::API_KEY, $storeId));
324
  }
325
 
326
  public function getSearchOnlyAPIKey($storeId = NULL)
327
  {
328
+ return trim(Mage::getStoreConfig(self::SEARCH_ONLY_API_KEY, $storeId));
329
  }
330
 
331
  public function getIndexPrefix($storeId = NULL)
332
  {
333
+ return trim(Mage::getStoreConfig(self::INDEX_PREFIX, $storeId));
334
+ }
335
+
336
+ public function getAttributesToRetrieve($group_id)
337
+ {
338
+ if (false === $this->isCustomerGroupsEnabled()) {
339
+ return [];
340
+ }
341
+
342
+ $attributes = array();
343
+ foreach ($this->getProductAdditionalAttributes() as $attribute) {
344
+ if ($attribute['attribute'] !== 'price') {
345
+ $attributes[] = $attribute['attribute'];
346
+ }
347
+ }
348
+
349
+ $attributes = array_merge($attributes, ['objectID', 'name', 'url', 'visibility_search', 'visibility_catalog', 'categories', 'categories_without_path', 'thumbnail_url', 'image_url', 'in_stock', 'type_id']);
350
+
351
+ $currencies = Mage::getModel('directory/currency')->getConfigAllowCurrencies();
352
+
353
+ foreach ($currencies as $currency) {
354
+ $attributes[] = 'price.'.$currency.'.default';
355
+ $attributes[] = 'price.'.$currency.'.default_formated';
356
+ $attributes[] = 'price.'.$currency.'.group_'.$group_id;
357
+ $attributes[] = 'price.'.$currency.'.group_'.$group_id.'_formated';
358
+ $attributes[] = 'price.'.$currency.'.special_from_date';
359
+ $attributes[] = 'price.'.$currency.'.special_to_date';
360
+ }
361
+
362
+
363
+ return ['attributesToRetrieve' => $attributes];
364
  }
365
 
366
  public function getCategoryAdditionalAttributes($storeId = NULL)
app/code/community/Algolia/Algoliasearch/Helper/Data.php CHANGED
@@ -7,6 +7,8 @@ if (class_exists('AlgoliaSearch\Client', false) == false)
7
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/ClientContext.php';
8
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Client.php';
9
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Index.php';
 
 
10
  }
11
 
12
  class Algolia_Algoliasearch_Helper_Data extends Mage_Core_Helper_Abstract
@@ -25,7 +27,7 @@ class Algolia_Algoliasearch_Helper_Data extends Mage_Core_Helper_Abstract
25
 
26
  public function __construct()
27
  {
28
- \AlgoliaSearch\Version::$custom_value = " Magento (1.5.4)";
29
 
30
  $this->algolia_helper = Mage::helper('algoliasearch/algoliahelper');
31
 
7
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/ClientContext.php';
8
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Client.php';
9
  require_once Mage::getBaseDir('lib').'/AlgoliaSearch/Index.php';
10
+ require_once Mage::getBaseDir('lib').'/AlgoliaSearch/PlacesIndex.php';
11
+ require_once Mage::getBaseDir('lib').'/AlgoliaSearch/IndexBrowser.php';
12
  }
13
 
14
  class Algolia_Algoliasearch_Helper_Data extends Mage_Core_Helper_Abstract
27
 
28
  public function __construct()
29
  {
30
+ \AlgoliaSearch\Version::$custom_value = " Magento (1.5.5)";
31
 
32
  $this->algolia_helper = Mage::helper('algoliasearch/algoliahelper');
33
 
app/code/community/Algolia/Algoliasearch/Helper/Entity/Additionalsectionshelper.php CHANGED
@@ -21,11 +21,13 @@ class Algolia_Algoliasearch_Helper_Entity_Additionalsectionshelper extends Algol
21
  $products = Mage::getResourceModel('catalog/product_collection')
22
  ->addStoreFilter($storeId)
23
  ->addAttributeToFilter('visibility', array('in' => Mage::getSingleton('catalog/product_visibility')->getVisibleInSearchIds()))
 
24
  ->addAttributeToFilter($attributeCode, array('notnull' => true))
25
  ->addAttributeToFilter($attributeCode, array('neq' => ''))
26
  ->addAttributeToSelect($attributeCode);
27
 
28
- $usedAttributeValues = array_unique($products->getColumnValues($attributeCode));
 
29
 
30
  $attributeModel = Mage::getSingleton('eav/config')
31
  ->getAttribute('catalog_product', $attributeCode)
@@ -45,7 +47,7 @@ class Algolia_Algoliasearch_Helper_Entity_Additionalsectionshelper extends Algol
45
  $values = array($values);
46
  }
47
 
48
- $values = array_map(function ($value) use ($section) {
49
 
50
  $record = array(
51
  'objectID' => $value,
@@ -54,7 +56,8 @@ class Algolia_Algoliasearch_Helper_Entity_Additionalsectionshelper extends Algol
54
 
55
  $transport = new Varien_Object($record);
56
 
57
- Mage::dispatchEvent('algolia_additional_section_item_index_before', array('section' => $section, 'record' => $transport));
 
58
 
59
  $record = $transport->getData();
60
 
@@ -63,4 +66,4 @@ class Algolia_Algoliasearch_Helper_Entity_Additionalsectionshelper extends Algol
63
 
64
  return $values;
65
  }
66
- }
21
  $products = Mage::getResourceModel('catalog/product_collection')
22
  ->addStoreFilter($storeId)
23
  ->addAttributeToFilter('visibility', array('in' => Mage::getSingleton('catalog/product_visibility')->getVisibleInSearchIds()))
24
+ ->addAttributeToFilter('status', array('eq' => Mage_Catalog_Model_Product_Status::STATUS_ENABLED))
25
  ->addAttributeToFilter($attributeCode, array('notnull' => true))
26
  ->addAttributeToFilter($attributeCode, array('neq' => ''))
27
  ->addAttributeToSelect($attributeCode);
28
 
29
+ $usedAttributeValues = array_keys(array_flip( // array unique
30
+ explode(',', implode(',', $products->getColumnValues($attributeCode)))));
31
 
32
  $attributeModel = Mage::getSingleton('eav/config')
33
  ->getAttribute('catalog_product', $attributeCode)
47
  $values = array($values);
48
  }
49
 
50
+ $values = array_map(function ($value) use ($section, $storeId) {
51
 
52
  $record = array(
53
  'objectID' => $value,
56
 
57
  $transport = new Varien_Object($record);
58
 
59
+ Mage::dispatchEvent('algolia_additional_section_item_index_before',
60
+ array('section' => $section, 'record' => $transport, 'store_id' => $storeId));
61
 
62
  $record = $transport->getData();
63
 
66
 
67
  return $values;
68
  }
69
+ }
app/code/community/Algolia/Algoliasearch/Helper/Entity/Categoryhelper.php CHANGED
@@ -66,13 +66,14 @@ class Algolia_Algoliasearch_Helper_Entity_Categoryhelper extends Algolia_Algolia
66
  foreach ($unserializedCategorysAttrs as $attr)
67
  $additionalAttr[] = $attr['attribute'];
68
 
 
 
69
  $categories
70
  ->addPathFilter($storeRootCategoryPath)
71
  ->addNameToResult()
72
  ->addUrlRewriteToResult()
73
  ->addIsActiveFilter()
74
  ->setStoreId($storeId)
75
- ->addAttributeToFilter('include_in_menu', '1')
76
  ->addAttributeToSelect(array_merge(array('name'), $additionalAttr))
77
  ->addFieldToFilter('level', array('gt' => 1));
78
 
@@ -140,14 +141,15 @@ class Algolia_Algoliasearch_Helper_Entity_Categoryhelper extends Algolia_Algolia
140
  } catch (Exception $e) { /* no image, no default: not fatal */
141
  }
142
  $data = array(
143
- 'objectID' => $category->getId(),
144
- 'name' => $category->getName(),
145
- 'path' => $path,
146
- 'level' => $category->getLevel(),
147
- 'url' => $category->getUrl(),
148
- '_tags' => array('category'),
149
- 'popularity' => 1,
150
- 'product_count' => $category->getProductCount()
 
151
  );
152
 
153
  if ( ! empty($image_url)) {
@@ -158,11 +160,11 @@ class Algolia_Algoliasearch_Helper_Entity_Categoryhelper extends Algolia_Algolia
158
  {
159
  $value = $category->getData($attribute['attribute']);
160
 
161
- $attribute_ressource = $category->getResource()->getAttribute($attribute['attribute']);
162
 
163
- if ($attribute_ressource)
164
  {
165
- $value = $attribute_ressource->getFrontend()->getValue($category);
166
  }
167
 
168
  if (isset($data[$attribute['attribute']]))
66
  foreach ($unserializedCategorysAttrs as $attr)
67
  $additionalAttr[] = $attr['attribute'];
68
 
69
+ $additionalAttr[] = 'include_in_menu';
70
+
71
  $categories
72
  ->addPathFilter($storeRootCategoryPath)
73
  ->addNameToResult()
74
  ->addUrlRewriteToResult()
75
  ->addIsActiveFilter()
76
  ->setStoreId($storeId)
 
77
  ->addAttributeToSelect(array_merge(array('name'), $additionalAttr))
78
  ->addFieldToFilter('level', array('gt' => 1));
79
 
141
  } catch (Exception $e) { /* no image, no default: not fatal */
142
  }
143
  $data = array(
144
+ 'objectID' => $category->getId(),
145
+ 'name' => $category->getName(),
146
+ 'path' => $path,
147
+ 'level' => $category->getLevel(),
148
+ 'url' => $category->getUrl(),
149
+ 'include_in_menu' => $category->getIncludeInMenu(),
150
+ '_tags' => array('category'),
151
+ 'popularity' => 1,
152
+ 'product_count' => $category->getProductCount()
153
  );
154
 
155
  if ( ! empty($image_url)) {
160
  {
161
  $value = $category->getData($attribute['attribute']);
162
 
163
+ $attribute_resource = $category->getResource()->getAttribute($attribute['attribute']);
164
 
165
+ if ($attribute_resource)
166
  {
167
+ $value = $attribute_resource->getFrontend()->getValue($category);
168
  }
169
 
170
  if (isset($data[$attribute['attribute']]))
app/code/community/Algolia/Algoliasearch/Helper/Entity/Helper.php CHANGED
@@ -70,6 +70,7 @@ abstract class Algolia_Algoliasearch_Helper_Entity_Helper
70
  $s = trim(preg_replace('/\s+/', ' ', $s));
71
  $s = preg_replace('/ /', ' ', $s);
72
  $s = preg_replace('!\s+!', ' ', $s);
 
73
 
74
  return trim(strip_tags($s));
75
  }
@@ -231,4 +232,4 @@ abstract class Algolia_Algoliasearch_Helper_Entity_Helper
231
  return $store_ids;
232
  }
233
 
234
- }
70
  $s = trim(preg_replace('/\s+/', ' ', $s));
71
  $s = preg_replace('/ /', ' ', $s);
72
  $s = preg_replace('!\s+!', ' ', $s);
73
+ $s = preg_replace('/\{\{[^}]+\}\}/', ' ', $s);
74
 
75
  return trim(strip_tags($s));
76
  }
232
  return $store_ids;
233
  }
234
 
235
+ }
app/code/community/Algolia/Algoliasearch/Helper/Entity/Pagehelper.php CHANGED
@@ -45,14 +45,19 @@ class Algolia_Algoliasearch_Helper_Entity_Pagehelper extends Algolia_Algoliasear
45
  if (! $page->getId())
46
  continue;
47
 
48
- $page_obj['objectID'] = $page->getId();
 
 
 
 
49
 
 
50
  $page_obj['url'] = Mage::helper('cms/page')->getPageUrl($page->getId());
51
- $page_obj['content'] = $this->strip($page->getContent());
52
 
53
  $pages[] = $page_obj;
54
  }
55
 
56
  return $pages;
57
  }
58
- }
45
  if (! $page->getId())
46
  continue;
47
 
48
+ $content = $page->getContent();
49
+ if ($this->config->getRenderTemplateDirectives()) {
50
+ $tmplProc = Mage::helper('cms')->getPageTemplateProcessor();
51
+ $content = $tmplProc->filter($content);
52
+ }
53
 
54
+ $page_obj['objectID'] = $page->getId();
55
  $page_obj['url'] = Mage::helper('cms/page')->getPageUrl($page->getId());
56
+ $page_obj['content'] = $this->strip($content);
57
 
58
  $pages[] = $page_obj;
59
  }
60
 
61
  return $pages;
62
  }
63
+ }
app/code/community/Algolia/Algoliasearch/Helper/Entity/Producthelper.php CHANGED
@@ -33,13 +33,7 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
33
 
34
  $productAttributes = array_merge(array('name', 'path', 'categories', 'categories_without_path', 'description', 'ordered_qty', 'total_ordered', 'stock_qty', 'rating_summary', 'media_gallery'), $allAttributes);
35
 
36
- $excludedAttributes = array(
37
- 'all_children', 'available_sort_by', 'children', 'children_count', 'custom_apply_to_products',
38
- 'custom_design', 'custom_design_from', 'custom_design_to', 'custom_layout_update', 'custom_use_parent_settings',
39
- 'default_sort_by', 'display_mode', 'filter_price_range', 'global_position', 'image', 'include_in_menu', 'is_active',
40
- 'is_always_include_in_menu', 'is_anchor', 'landing_page', 'level', 'lower_cms_block',
41
- 'page_layout', 'path_in_store', 'position', 'small_image', 'thumbnail', 'url_key', 'url_path',
42
- 'visible_in_menu');
43
 
44
  $productAttributes = array_diff($productAttributes, $excludedAttributes);
45
 
@@ -59,6 +53,18 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
59
  return $attributes;
60
  }
61
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  public function isAttributeEnabled($additionalAttributes, $attr_name)
63
  {
64
  foreach ($additionalAttributes as $attr)
@@ -76,21 +82,26 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
76
  $products = $products->setStoreId($storeId)
77
  ->addStoreFilter($storeId);
78
 
79
- if ($only_visible)
80
  $products = $products->addAttributeToFilter('visibility', array('in' => Mage::getSingleton('catalog/product_visibility')->getVisibleInSiteIds()));
 
 
 
81
 
82
- if (false === $this->config->getShowOutOfStock($storeId))
83
  Mage::getSingleton('cataloginventory/stock')->addInStockFilterToCollection($products);
 
84
 
85
- $products = $products->addFinalPrice()
86
- ->addAttributeToSelect('special_from_date')
87
  ->addAttributeToSelect('special_to_date')
88
  ->addAttributeToFilter('status', Mage_Catalog_Model_Product_Status::STATUS_ENABLED);
89
 
90
  $additionalAttr = $this->config->getProductAdditionalAttributes($storeId);
91
 
92
- foreach ($additionalAttr as &$attr)
93
- $attr = $attr['attribute'];
 
 
94
 
95
  $products = $products->addAttributeToSelect(array_values(array_merge(static::$_predefinedProductAttributes, $additionalAttr)));
96
 
@@ -197,18 +208,15 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
197
 
198
  foreach ($sorting_indices as $values)
199
  {
200
- if ($this->config->isCustomerGroupsEnabled($storeId))
201
  {
202
- if ($values['attribute'] === 'price')
203
  {
204
- foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group)
205
- {
206
- $group_id = (int)$group->getData('customer_group_id');
207
-
208
- $suffix_index_name = 'group_' . $group_id;
209
-
210
- $slaves[] = $this->getIndexName($storeId) . '_' .$values['attribute'].'_' . $suffix_index_name . '_' . $values['sort'];
211
- }
212
  }
213
  }
214
  else
@@ -224,7 +232,7 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
224
 
225
  foreach ($sorting_indices as $values)
226
  {
227
- if ($this->config->isCustomerGroupsEnabled($storeId) && strpos($values['attribute'], 'price') !== false)
228
  {
229
  foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group)
230
  {
@@ -232,7 +240,7 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
232
 
233
  $suffix_index_name = 'group_' . $group_id;
234
 
235
- $sort_attribute = strpos($values['attribute'], 'price') !== false ? $values['attribute'] . '.' . $currencies[0] . '.' . $suffix_index_name : $values['attribute'];
236
 
237
  $mergeSettings['ranking'] = array($values['sort'] . '(' . $sort_attribute . ')', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom');
238
 
@@ -241,7 +249,7 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
241
  }
242
  else
243
  {
244
- $sort_attribute = strpos($values['attribute'], 'price') !== false ? $values['attribute'] . '.' . $currencies[0] . '.' . 'default' : $values['attribute'];
245
 
246
  $mergeSettings['ranking'] = array($values['sort'] . '(' . $sort_attribute . ')', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom');
247
 
@@ -468,6 +476,15 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
468
  }
469
  }
470
 
 
 
 
 
 
 
 
 
 
471
  public function getObject(Mage_Catalog_Model_Product $product)
472
  {
473
  $type = $this->config->getMappedProductType($product->getTypeId());
@@ -512,7 +529,6 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
512
  $categoryCollection = Mage::getResourceModel('catalog/category_collection')
513
  ->addAttributeToSelect('name')
514
  ->addAttributeToFilter('entity_id', $_categoryIds)
515
- ->addAttributeToFilter('include_in_menu', '1')
516
  ->addFieldToFilter('level', array('gt' => 1))
517
  ->addIsActiveFilter();
518
 
@@ -684,23 +700,28 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
684
 
685
  foreach ($additionalAttributes as $attribute)
686
  {
687
- if (isset($customData[$attribute['attribute']]))
 
688
  continue;
689
 
690
- $value = $product->getData($attribute['attribute']);
691
 
692
- $attribute_ressource = $product->getResource()->getAttribute($attribute['attribute']);
693
 
694
- if ($attribute_ressource)
695
  {
696
- $attribute_ressource = $attribute_ressource->setStoreId($product->getStoreId());
697
 
698
- if ($value === null)
699
  {
700
  /** Get values as array in children */
701
  if ($type == 'configurable' || $type == 'grouped' || $type == 'bundle')
702
  {
703
- $values = array();
 
 
 
 
704
 
705
  $all_sub_products_out_of_stock = true;
706
 
@@ -708,27 +729,22 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
708
  {
709
  $isInStock = (int) $sub_product->getStockItem()->getIsInStock();
710
 
711
- if ($isInStock == false)
712
  continue;
713
 
714
  $all_sub_products_out_of_stock = false;
715
 
716
- $value = $sub_product->getData($attribute['attribute']);
717
 
718
  if ($value)
719
  {
720
- $value_text = $sub_product->getAttributeText($attribute['attribute']);
721
-
722
- if ($value_text)
723
- $values[] = $value_text;
724
- else
725
- $values[] = $attribute_ressource->getFrontend()->getValue($sub_product);
726
  }
727
  }
728
 
729
  if (is_array($values) && count($values) > 0)
730
  {
731
- $customData[$attribute['attribute']] = array_values(array_unique($values));
732
  }
733
 
734
  if ($customData['in_stock'] && $all_sub_products_out_of_stock) {
@@ -740,19 +756,11 @@ class Algolia_Algoliasearch_Helper_Entity_Producthelper extends Algolia_Algolias
740
  }
741
  else
742
  {
743
- $value_text = $product->getAttributeText($attribute['attribute']);
744
-
745
- if ($value_text)
746
- $value = $value_text;
747
- else
748
- {
749
- $attribute_ressource = $attribute_ressource->setStoreId($product->getStoreId());
750
- $value = $attribute_ressource->getFrontend()->getValue($product);
751
- }
752
 
753
  if ($value)
754
  {
755
- $customData[$attribute['attribute']] = $value;
756
  }
757
  }
758
  }
33
 
34
  $productAttributes = array_merge(array('name', 'path', 'categories', 'categories_without_path', 'description', 'ordered_qty', 'total_ordered', 'stock_qty', 'rating_summary', 'media_gallery'), $allAttributes);
35
 
36
+ $excludedAttributes = $this->getExcludedAttributes();
 
 
 
 
 
 
37
 
38
  $productAttributes = array_diff($productAttributes, $excludedAttributes);
39
 
53
  return $attributes;
54
  }
55
 
56
+ protected function getExcludedAttributes()
57
+ {
58
+ return array(
59
+ 'all_children', 'available_sort_by', 'children', 'children_count', 'custom_apply_to_products',
60
+ 'custom_design', 'custom_design_from', 'custom_design_to', 'custom_layout_update', 'custom_use_parent_settings',
61
+ 'default_sort_by', 'display_mode', 'filter_price_range', 'global_position', 'image', 'include_in_menu', 'is_active',
62
+ 'is_always_include_in_menu', 'is_anchor', 'landing_page', 'level', 'lower_cms_block',
63
+ 'page_layout', 'path_in_store', 'position', 'small_image', 'thumbnail', 'url_key', 'url_path',
64
+ 'visible_in_menu'
65
+ );
66
+ }
67
+
68
  public function isAttributeEnabled($additionalAttributes, $attr_name)
69
  {
70
  foreach ($additionalAttributes as $attr)
82
  $products = $products->setStoreId($storeId)
83
  ->addStoreFilter($storeId);
84
 
85
+ if ($only_visible) {
86
  $products = $products->addAttributeToFilter('visibility', array('in' => Mage::getSingleton('catalog/product_visibility')->getVisibleInSiteIds()));
87
+ $products = $products->addFinalPrice();
88
+ }
89
+
90
 
91
+ if (false === $this->config->getShowOutOfStock($storeId) && $only_visible == true) {
92
  Mage::getSingleton('cataloginventory/stock')->addInStockFilterToCollection($products);
93
+ }
94
 
95
+ $products = $products->addAttributeToSelect('special_from_date')
 
96
  ->addAttributeToSelect('special_to_date')
97
  ->addAttributeToFilter('status', Mage_Catalog_Model_Product_Status::STATUS_ENABLED);
98
 
99
  $additionalAttr = $this->config->getProductAdditionalAttributes($storeId);
100
 
101
+ /** Map instead of foreach because otherwise it adds quotes to the last attribute **/
102
+ $additionalAttr = array_map(function($attr) {
103
+ return $attr['attribute'];
104
+ }, $additionalAttr);
105
 
106
  $products = $products->addAttributeToSelect(array_values(array_merge(static::$_predefinedProductAttributes, $additionalAttr)));
107
 
208
 
209
  foreach ($sorting_indices as $values)
210
  {
211
+ if ($this->config->isCustomerGroupsEnabled($storeId) && $values['attribute'] === 'price')
212
  {
213
+ foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group)
214
  {
215
+ $group_id = (int) $group->getData('customer_group_id');
216
+
217
+ $suffix_index_name = 'group_'.$group_id;
218
+
219
+ $slaves[] = $this->getIndexName($storeId).'_'.$values['attribute'].'_'.$suffix_index_name.'_'.$values['sort'];
 
 
 
220
  }
221
  }
222
  else
232
 
233
  foreach ($sorting_indices as $values)
234
  {
235
+ if ($this->config->isCustomerGroupsEnabled($storeId) && $values['attribute'] === 'price')
236
  {
237
  foreach ($groups = Mage::getModel('customer/group')->getCollection() as $group)
238
  {
240
 
241
  $suffix_index_name = 'group_' . $group_id;
242
 
243
+ $sort_attribute = $values['attribute'] === 'price' ? $values['attribute'] . '.' . $currencies[0] . '.' . $suffix_index_name : $values['attribute'];
244
 
245
  $mergeSettings['ranking'] = array($values['sort'] . '(' . $sort_attribute . ')', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom');
246
 
249
  }
250
  else
251
  {
252
+ $sort_attribute = $values['attribute'] === 'price' ? $values['attribute'] . '.' . $currencies[0] . '.' . 'default' : $values['attribute'];
253
 
254
  $mergeSettings['ranking'] = array($values['sort'] . '(' . $sort_attribute . ')', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom');
255
 
476
  }
477
  }
478
 
479
+ protected function getValueOrValueText(Mage_Catalog_Model_Product $product, $name, $resource)
480
+ {
481
+ $value_text = $product->getAttributeText($name);
482
+ if (!$value_text) {
483
+ $value_text = $resource->getFrontend()->getValue($product);
484
+ }
485
+ return $value_text;
486
+ }
487
+
488
  public function getObject(Mage_Catalog_Model_Product $product)
489
  {
490
  $type = $this->config->getMappedProductType($product->getTypeId());
529
  $categoryCollection = Mage::getResourceModel('catalog/category_collection')
530
  ->addAttributeToSelect('name')
531
  ->addAttributeToFilter('entity_id', $_categoryIds)
 
532
  ->addFieldToFilter('level', array('gt' => 1))
533
  ->addIsActiveFilter();
534
 
700
 
701
  foreach ($additionalAttributes as $attribute)
702
  {
703
+ $attribute_name = $attribute['attribute'];
704
+ if (isset($customData[$attribute_name]))
705
  continue;
706
 
707
+ $value = $product->getData($attribute_name);
708
 
709
+ $attribute_resource = $product->getResource()->getAttribute($attribute_name);
710
 
711
+ if ($attribute_resource)
712
  {
713
+ $attribute_resource->setStoreId($product->getStoreId());
714
 
715
+ if ($value === null || 'sku' == $attribute_name)
716
  {
717
  /** Get values as array in children */
718
  if ($type == 'configurable' || $type == 'grouped' || $type == 'bundle')
719
  {
720
+ if ($value === null) {
721
+ $values = array();
722
+ } else {
723
+ $values = array($this->getValueOrValueText($product, $attribute_name, $attribute_resource));
724
+ }
725
 
726
  $all_sub_products_out_of_stock = true;
727
 
729
  {
730
  $isInStock = (int) $sub_product->getStockItem()->getIsInStock();
731
 
732
+ if ($isInStock == false && $this->config->indexOutOfStockOptions($product->getStoreId()) == false)
733
  continue;
734
 
735
  $all_sub_products_out_of_stock = false;
736
 
737
+ $value = $sub_product->getData($attribute_name);
738
 
739
  if ($value)
740
  {
741
+ $values[] = $this->getValueOrValueText($sub_product, $attribute_name, $attribute_resource);
 
 
 
 
 
742
  }
743
  }
744
 
745
  if (is_array($values) && count($values) > 0)
746
  {
747
+ $customData[$attribute_name] = array_values(array_unique($values));
748
  }
749
 
750
  if ($customData['in_stock'] && $all_sub_products_out_of_stock) {
756
  }
757
  else
758
  {
759
+ $value = $this->getValueOrValueText($product, $attribute_name, $attribute_resource);
 
 
 
 
 
 
 
 
760
 
761
  if ($value)
762
  {
763
+ $customData[$attribute_name] = $value;
764
  }
765
  }
766
  }
app/code/community/Algolia/Algoliasearch/Helper/Entity/Suggestionhelper.php CHANGED
@@ -2,6 +2,9 @@
2
 
3
  class Algolia_Algoliasearch_Helper_Entity_Suggestionhelper extends Algolia_Algoliasearch_Helper_Entity_Helper
4
  {
 
 
 
5
  protected function getIndexNameSuffix()
6
  {
7
  return '_suggestions';
@@ -32,27 +35,50 @@ class Algolia_Algoliasearch_Helper_Entity_Suggestionhelper extends Algolia_Algol
32
 
33
  public function getPopularQueries($storeId)
34
  {
35
- $collection = Mage::getResourceModel('catalogsearch/query_collection');
36
- $collection->getSelect()->where('num_results >= '.$this->config->getMinNumberOfResults().' AND popularity >= ' . $this->config->getMinPopularity() .' AND query_text != "__empty__"');
37
- $collection->getSelect()->limit(12);
38
- $collection->setOrder('popularity', 'DESC');
39
- $collection->setOrder('num_results', 'DESC');
40
- $collection->setOrder('updated_at', 'ASC');
41
 
42
- if ($storeId) {
43
- $collection->getSelect()->where('store_id = ?', (int) $storeId);
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- $collection->load();
47
 
48
- $suggestions = array();
49
 
50
- /** @var $suggestion Mage_Catalog_Model_Category */
51
- foreach ($collection as $suggestion)
52
- if (strlen($suggestion['query_text']) >= 3)
53
- $suggestions[] = $suggestion['query_text'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- return array_slice($suggestions, 0, 9);
56
  }
57
 
58
  public function getSuggestionCollectionQuery($storeId)
2
 
3
  class Algolia_Algoliasearch_Helper_Entity_Suggestionhelper extends Algolia_Algoliasearch_Helper_Entity_Helper
4
  {
5
+ protected $_popularQueries = null;
6
+ protected $_popularQueriesCacheId = "algoliasearch_popular_queries_cache_tag";
7
+
8
  protected function getIndexNameSuffix()
9
  {
10
  return '_suggestions';
35
 
36
  public function getPopularQueries($storeId)
37
  {
38
+ if ($this->_popularQueries == null) {
 
 
 
 
 
39
 
40
+ // load from cache if we can
41
+ $cachedPopularQueries = Mage::app()->loadCache($this->_popularQueriesCacheId);
42
+ if ($cachedPopularQueries) {
43
+ $this->_popularQueries = unserialize($cachedPopularQueries);
44
+ } else {
45
+ $collection = Mage::getResourceModel('catalogsearch/query_collection');
46
+ $collection->getSelect()->where('num_results >= ' . $this->config->getMinNumberOfResults() . ' AND popularity >= ' . $this->config->getMinPopularity() . ' AND query_text != "__empty__"');
47
+ $collection->getSelect()->limit(12);
48
+ $collection->setOrder('popularity', 'DESC');
49
+ $collection->setOrder('num_results', 'DESC');
50
+ $collection->setOrder('updated_at', 'ASC');
51
+
52
+ if ($storeId) {
53
+ $collection->getSelect()->where('store_id = ?', (int)$storeId);
54
+ }
55
 
56
+ $collection->load();
57
 
58
+ $suggestions = array();
59
 
60
+ /** @var $suggestion Mage_Catalog_Model_Category */
61
+ foreach ($collection as $suggestion)
62
+ if (strlen($suggestion['query_text']) >= 3)
63
+ $suggestions[] = $suggestion['query_text'];
64
+
65
+ $this->_popularQueries = array_slice($suggestions, 0, 9);
66
+ try { //save to cache
67
+ $cacheContent = serialize($this->_popularQueries);
68
+ $tags = array(
69
+ Mage_CatalogSearch_Model_Query::CACHE_TAG
70
+ );
71
+
72
+ Mage::app()->saveCache($cacheContent, $this->_popularQueriesCacheId, $tags, 604800);
73
+
74
+ } catch (Exception $e) {
75
+ // Exception = no caching
76
+ Mage::logException($e);
77
+ }
78
+ }
79
+ }
80
 
81
+ return $this->_popularQueries;
82
  }
83
 
84
  public function getSuggestionCollectionQuery($storeId)
app/code/community/Algolia/Algoliasearch/Helper/Logger.php CHANGED
@@ -51,9 +51,9 @@ class Algolia_Algoliasearch_Helper_Logger extends Mage_Core_Helper_Abstract
51
  $this->log('<<<<< END ' .$action. ' (' . $this->formatTime($this->timers[$action], microtime(true)) . ')');
52
  }
53
 
54
- public function log($message)
55
  {
56
- if ($this->config->isLoggingEnabled()) {
57
  Mage::log($message, null, 'algolia.log');
58
  }
59
  }
51
  $this->log('<<<<< END ' .$action. ' (' . $this->formatTime($this->timers[$action], microtime(true)) . ')');
52
  }
53
 
54
+ public function log($message, $forceLog = false)
55
  {
56
+ if ($this->config->isLoggingEnabled() || $forceLog) {
57
  Mage::log($message, null, 'algolia.log');
58
  }
59
  }
app/code/community/Algolia/Algoliasearch/Model/Indexer/Algolia.php CHANGED
@@ -109,19 +109,16 @@ class Algolia_Algoliasearch_Model_Indexer_Algolia extends Mage_Index_Model_Index
109
 
110
  $product = Mage::getModel('catalog/product')->load($object->getProductId());
111
 
112
- if ($object->getData('is_in_stock') == false || (int) $product->getStockItem()->getQty() <= 0)
113
  {
114
- try // In case of wrong credentials or overquota or block account. To avoid checkout process to fail
115
- {
116
- $event->addNewData('catalogsearch_delete_product_id', $product->getId());
117
- $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds());
118
- }
119
- catch(\Exception $e)
120
- {
121
- $this->logger->log('Error while trying to update stock');
122
- $this->logger->log($e->getMessage());
123
- $this->logger->log($e->getTraceAsString());
124
- }
125
  }
126
  }
127
  }
@@ -132,84 +129,33 @@ class Algolia_Algoliasearch_Model_Indexer_Algolia extends Mage_Index_Model_Index
132
  case Mage_Index_Model_Event::TYPE_SAVE:
133
  /** @var $product Mage_Catalog_Model_Product */
134
  $product = $event->getDataObject();
135
- $delete = FALSE;
136
- $visibleInSite = Mage::getSingleton('catalog/product_visibility')->getVisibleInSiteIds();
137
-
138
- if ($product->getStatus() == Mage_Catalog_Model_Product_Status::STATUS_DISABLED)
139
- {
140
- $delete = TRUE;
141
- }
142
- elseif (! in_array($product->getData('visibility'), $visibleInSite))
143
- {
144
- $delete = TRUE;
145
- }
146
 
147
- if ($delete)
148
- {
149
- $event->addNewData('catalogsearch_delete_product_id', $product->getId());
150
- $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds());
151
- }
152
- else
153
  {
154
- $event->addNewData('catalogsearch_update_product_id', $product->getId());
 
155
 
156
- /* product_categories is filled in Observer::saveProduct */
157
- if (isset(static::$product_categories[$product->getId()]))
158
- {
159
- $oldCategories = static::$product_categories[$product->getId()];
160
- $newCategories = $product->getCategoryIds();
161
 
162
- $diffCategories = array_merge(array_diff($oldCategories, $newCategories), array_diff($newCategories, $oldCategories));
163
-
164
- $event->addNewData('catalogsearch_update_category_id', $diffCategories);
165
- }
166
  }
167
 
168
- break;
169
  case Mage_Index_Model_Event::TYPE_DELETE:
170
 
171
  /** @var $product Mage_Catalog_Model_Product */
172
  $product = $event->getDataObject();
173
- $event->addNewData('catalogsearch_delete_product_id', $product->getId());
174
  $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds());
175
-
176
  break;
 
177
  case Mage_Index_Model_Event::TYPE_MASS_ACTION:
178
  /** @var $actionObject Varien_Object */
179
  $actionObject = $event->getDataObject();
180
 
181
- $reindexData = array();
182
-
183
- // Check if status changed
184
- $attrData = $actionObject->getAttributesData();
185
-
186
- if (isset($attrData['status']))
187
- {
188
- $reindexData['catalogsearch_status'] = $attrData['status'];
189
- }
190
-
191
- // Check changed websites
192
- if ($actionObject->getWebsiteIds())
193
- {
194
- $reindexData['catalogsearch_website_ids'] = $actionObject->getWebsiteIds();
195
- $reindexData['catalogsearch_action_type'] = $actionObject->getActionType();
196
- }
197
-
198
- $reindexData['catalogsearch_force_reindex'] = TRUE;
199
-
200
- if ($actionObject->getIsDeleted())
201
- {
202
- $reindexData['catalogsearch_delete_product_id'] = $actionObject->getProductIds();
203
- }
204
- else
205
- {
206
- $reindexData['catalogsearch_product_ids'] = $actionObject->getProductIds();
207
- }
208
-
209
- foreach ($reindexData as $k => $v)
210
- {
211
- $event->addNewData($k, $v);
212
- }
213
 
214
  break;
215
  }
@@ -247,81 +193,18 @@ class Algolia_Algoliasearch_Model_Indexer_Algolia extends Mage_Index_Model_Index
247
  $process->changeStatus(Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX);
248
  }
249
 
250
- /*
251
- * Clear index for the deleted product.
252
- */
253
- else if ( ! empty($data['catalogsearch_delete_product_id'])) {
254
- $productId = $data['catalogsearch_delete_product_id'];
255
-
256
- if ( ! $this->_isProductComposite($productId)) {
257
- $parentIds = $this->_getResource()->getRelationsByChild($productId);
258
- if ( ! empty($parentIds)) {
259
- $this->engine
260
- ->rebuildProductIndex(null, $parentIds);
261
- }
262
- }
263
-
264
- $this->engine
265
- ->removeProducts(null, $productId);
266
- }
267
-
268
- // Mass action
269
- else if ( ! empty($data['catalogsearch_product_ids'])) {
270
- $productIds = $data['catalogsearch_product_ids'];
271
-
272
- if (!empty($productIds))
273
- {
274
- if ( ! empty($data['catalogsearch_website_ids']))
275
- {
276
- $websiteIds = $data['catalogsearch_website_ids'];
277
- $actionType = $data['catalogsearch_action_type'];
278
- foreach ($websiteIds as $websiteId)
279
- {
280
- foreach (Mage::app()->getWebsite($websiteId)->getStoreIds() as $storeId) {
281
- if ($actionType == 'remove')
282
- {
283
- $this->engine->removeProducts($storeId, $productIds);
284
- }
285
- else if ($actionType == 'add')
286
- {
287
- $this->engine->rebuildProductIndex($storeId, $productIds);
288
- }
289
- }
290
- }
291
- }
292
- else if (isset($data['catalogsearch_status']))
293
- {
294
- $status = $data['catalogsearch_status'];
295
- if ($status == Mage_Catalog_Model_Product_Status::STATUS_ENABLED) {
296
- $this->engine->rebuildProductIndex(null, $productIds);
297
- }
298
- else
299
- {
300
- $this->engine->removeProducts(null, $productIds);
301
- }
302
- }
303
- else if (isset($data['catalogsearch_force_reindex']))
304
- {
305
- $this->engine
306
- ->rebuildProductIndex(null, $productIds);
307
- }
308
- }
309
- }
310
-
311
  if ( ! empty($data['catalogsearch_update_category_id'])) {
312
  $updateCategoryIds = $data['catalogsearch_update_category_id'];
313
  $updateCategoryIds = is_array($updateCategoryIds) ? $updateCategoryIds : array($updateCategoryIds);
314
 
315
- foreach ($updateCategoryIds as $id)
316
- {
317
  $categories = Mage::getModel('catalog/category')->getCategories($id);
318
 
319
  foreach ($categories as $category)
320
  $updateCategoryIds[] = $category->getId();
321
  }
322
 
323
- $this->engine
324
- ->rebuildCategoryIndex(null, $updateCategoryIds);
325
  }
326
 
327
  /*
@@ -333,20 +216,18 @@ class Algolia_Algoliasearch_Model_Indexer_Algolia extends Mage_Index_Model_Index
333
  $updateProductIds = is_array($updateProductIds) ? $updateProductIds : array($updateProductIds);
334
  $productIds = $updateProductIds;
335
 
336
- foreach ($updateProductIds as $updateProductId)
337
- {
338
- if (! $this->_isProductComposite($updateProductId))
339
- {
340
  $parentIds = $this->_getResource()->getRelationsByChild($updateProductId);
341
 
342
- if (! empty($parentIds))
343
- {
344
  $productIds = array_merge($productIds, $parentIds);
345
  }
346
  }
347
  }
348
 
349
  if (!empty($productIds)) {
 
350
  $this->engine->rebuildProductIndex(null, $productIds);
351
  }
352
  }
109
 
110
  $product = Mage::getModel('catalog/product')->load($object->getProductId());
111
 
112
+ try // In case of wrong credentials or overquota or block account. To avoid checkout process to fail
113
  {
114
+ $event->addNewData('catalogsearch_delete_product_id', $product->getId());
115
+ $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds());
116
+ }
117
+ catch(\Exception $e)
118
+ {
119
+ $this->logger->log('Error while trying to update stock');
120
+ $this->logger->log($e->getMessage());
121
+ $this->logger->log($e->getTraceAsString());
 
 
 
122
  }
123
  }
124
  }
129
  case Mage_Index_Model_Event::TYPE_SAVE:
130
  /** @var $product Mage_Catalog_Model_Product */
131
  $product = $event->getDataObject();
132
+ $event->addNewData('catalogsearch_update_product_id', $product->getId());
133
+ $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds());
 
 
 
 
 
 
 
 
 
134
 
135
+ /* product_categories is filled in Observer::saveProduct */
136
+ if (isset(static::$product_categories[$product->getId()]))
 
 
 
 
137
  {
138
+ $oldCategories = static::$product_categories[$product->getId()];
139
+ $newCategories = $product->getCategoryIds();
140
 
141
+ $diffCategories = array_merge(array_diff($oldCategories, $newCategories), array_diff($newCategories, $oldCategories));
 
 
 
 
142
 
143
+ $event->addNewData('catalogsearch_update_category_id', $diffCategories);
 
 
 
144
  }
145
 
 
146
  case Mage_Index_Model_Event::TYPE_DELETE:
147
 
148
  /** @var $product Mage_Catalog_Model_Product */
149
  $product = $event->getDataObject();
150
+ $event->addNewData('catalogsearch_update_product_id', $product->getId());
151
  $event->addNewData('catalogsearch_update_category_id', $product->getCategoryIds());
 
152
  break;
153
+
154
  case Mage_Index_Model_Event::TYPE_MASS_ACTION:
155
  /** @var $actionObject Varien_Object */
156
  $actionObject = $event->getDataObject();
157
 
158
+ $event->addNewData('catalogsearch_update_product_id', $actionObject->getProductIds());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  break;
161
  }
193
  $process->changeStatus(Mage_Index_Model_Process::STATUS_REQUIRE_REINDEX);
194
  }
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  if ( ! empty($data['catalogsearch_update_category_id'])) {
197
  $updateCategoryIds = $data['catalogsearch_update_category_id'];
198
  $updateCategoryIds = is_array($updateCategoryIds) ? $updateCategoryIds : array($updateCategoryIds);
199
 
200
+ foreach ($updateCategoryIds as $id) {
 
201
  $categories = Mage::getModel('catalog/category')->getCategories($id);
202
 
203
  foreach ($categories as $category)
204
  $updateCategoryIds[] = $category->getId();
205
  }
206
 
207
+ $this->engine->rebuildCategoryIndex(null, $updateCategoryIds);
 
208
  }
209
 
210
  /*
216
  $updateProductIds = is_array($updateProductIds) ? $updateProductIds : array($updateProductIds);
217
  $productIds = $updateProductIds;
218
 
219
+ foreach ($updateProductIds as $updateProductId) {
220
+ if (! $this->_isProductComposite($updateProductId)) {
 
 
221
  $parentIds = $this->_getResource()->getRelationsByChild($updateProductId);
222
 
223
+ if (! empty($parentIds)) {
 
224
  $productIds = array_merge($productIds, $parentIds);
225
  }
226
  }
227
  }
228
 
229
  if (!empty($productIds)) {
230
+ $this->engine->removeProducts(null, $productIds);
231
  $this->engine->rebuildProductIndex(null, $productIds);
232
  }
233
  }
app/code/community/Algolia/Algoliasearch/Model/Indexer/Algoliacategories.php CHANGED
@@ -70,7 +70,7 @@ class Algolia_Algoliasearch_Model_Indexer_Algoliacategories extends Mage_Index_M
70
  $category = $event->getDataObject();
71
  $productIds = $category->getAffectedProductIds();
72
 
73
- if (! $category->getData('is_active') || ! $category->getData('include_in_menu'))
74
  {
75
  $event->addNewData('catalogsearch_delete_category_id', array_merge(array($category->getId()), $category->getAllChildren(TRUE)));
76
 
70
  $category = $event->getDataObject();
71
  $productIds = $category->getAffectedProductIds();
72
 
73
+ if (! $category->getData('is_active'))
74
  {
75
  $event->addNewData('catalogsearch_delete_category_id', array_merge(array($category->getId()), $category->getAllChildren(TRUE)));
76
 
app/code/community/Algolia/Algoliasearch/Model/Resource/Fulltext/Collection.php CHANGED
@@ -20,7 +20,19 @@ class Algolia_Algoliasearch_Model_Resource_Fulltext_Collection extends Mage_Cata
20
  if ($config->isInstantEnabled($storeId) === false || $config->makeSeoRequest($storeId))
21
  {
22
  $algolia_query = $query !== '__empty__' ? $query : '';
23
- $data = Mage::helper('algoliasearch')->getSearchResult($algolia_query, $storeId);
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
 
20
  if ($config->isInstantEnabled($storeId) === false || $config->makeSeoRequest($storeId))
21
  {
22
  $algolia_query = $query !== '__empty__' ? $query : '';
23
+
24
+ try
25
+ {
26
+ $data = Mage::helper('algoliasearch')->getSearchResult($algolia_query, $storeId);
27
+ }
28
+ catch (\Exception $e)
29
+ {
30
+ $logger = Mage::helper('algoliasearch/logger');
31
+ $logger->log($e->getMessage(), true);
32
+ $logger->log($e->getTraceAsString(), true);
33
+
34
+ return parent::addSearchFilter($query);
35
+ }
36
  }
37
 
38
 
app/code/community/Algolia/Algoliasearch/etc/config.xml CHANGED
@@ -2,7 +2,7 @@
2
  <config>
3
  <modules>
4
  <Algolia_Algoliasearch>
5
- <version>1.5.4</version>
6
  </Algolia_Algoliasearch>
7
  </modules>
8
  <frontend>
@@ -151,6 +151,7 @@
151
  <custom_ranking_product_attributes>a:1:{s:18:"_1427960305274_274";a:2:{s:9:"attribute";s:11:"ordered_qty";s:5:"order";s:4:"desc";}}</custom_ranking_product_attributes>
152
  <results_limit>1000</results_limit>
153
  <show_suggestions_on_no_result_page>1</show_suggestions_on_no_result_page>
 
154
  </products>
155
  <instant>
156
  <replace_categories>1</replace_categories>
@@ -167,11 +168,13 @@
167
  <sections>a:1:{s:18:"_1450089283397_397";a:3:{s:4:"name";s:5:"pages";s:5:"label";s:5:"Pages";s:11:"hitsPerPage";s:1:"2";}}</sections>
168
  <min_popularity>1000</min_popularity>
169
  <min_number_of_results>2</min_number_of_results>
 
170
  </autocomplete>
171
  <categories>
172
  <number_category_suggestions>5</number_category_suggestions>
173
  <category_additional_attributes2>a:7:{s:18:"_1427960339954_954";a:4:{s:9:"attribute";s:4:"name";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427960354437_437";a:4:{s:9:"attribute";s:4:"path";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961004989_989";a:4:{s:9:"attribute";s:11:"description";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:9:"unordered";}s:18:"_1427961205511_511";a:4:{s:9:"attribute";s:10:"meta_title";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961216134_134";a:4:{s:9:"attribute";s:13:"meta_keywords";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961216916_916";a:4:{s:9:"attribute";s:16:"meta_description";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:9:"unordered";}s:18:"_1427977778338_338";a:4:{s:9:"attribute";s:13:"product_count";s:10:"searchable";s:1:"0";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}}</category_additional_attributes2>
174
  <custom_ranking_category_attributes>a:1:{s:18:"_1427961035192_192";a:2:{s:9:"attribute";s:13:"product_count";s:5:"order";s:4:"desc";}}</custom_ranking_category_attributes>
 
175
  </categories>
176
  <queue>
177
  <active>0</active>
@@ -199,4 +202,4 @@
199
  </product_map>
200
  </algoliasearch>
201
  </default>
202
- </config>
2
  <config>
3
  <modules>
4
  <Algolia_Algoliasearch>
5
+ <version>1.5.5</version>
6
  </Algolia_Algoliasearch>
7
  </modules>
8
  <frontend>
151
  <custom_ranking_product_attributes>a:1:{s:18:"_1427960305274_274";a:2:{s:9:"attribute";s:11:"ordered_qty";s:5:"order";s:4:"desc";}}</custom_ranking_product_attributes>
152
  <results_limit>1000</results_limit>
153
  <show_suggestions_on_no_result_page>1</show_suggestions_on_no_result_page>
154
+ <index_out_of_stock_options>0</index_out_of_stock_options>
155
  </products>
156
  <instant>
157
  <replace_categories>1</replace_categories>
168
  <sections>a:1:{s:18:"_1450089283397_397";a:3:{s:4:"name";s:5:"pages";s:5:"label";s:5:"Pages";s:11:"hitsPerPage";s:1:"2";}}</sections>
169
  <min_popularity>1000</min_popularity>
170
  <min_number_of_results>2</min_number_of_results>
171
+ <render_template_directives>1</render_template_directives>
172
  </autocomplete>
173
  <categories>
174
  <number_category_suggestions>5</number_category_suggestions>
175
  <category_additional_attributes2>a:7:{s:18:"_1427960339954_954";a:4:{s:9:"attribute";s:4:"name";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427960354437_437";a:4:{s:9:"attribute";s:4:"path";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961004989_989";a:4:{s:9:"attribute";s:11:"description";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:9:"unordered";}s:18:"_1427961205511_511";a:4:{s:9:"attribute";s:10:"meta_title";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961216134_134";a:4:{s:9:"attribute";s:13:"meta_keywords";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}s:18:"_1427961216916_916";a:4:{s:9:"attribute";s:16:"meta_description";s:10:"searchable";s:1:"1";s:11:"retrievable";s:1:"1";s:5:"order";s:9:"unordered";}s:18:"_1427977778338_338";a:4:{s:9:"attribute";s:13:"product_count";s:10:"searchable";s:1:"0";s:11:"retrievable";s:1:"1";s:5:"order";s:7:"ordered";}}</category_additional_attributes2>
176
  <custom_ranking_category_attributes>a:1:{s:18:"_1427961035192_192";a:2:{s:9:"attribute";s:13:"product_count";s:5:"order";s:4:"desc";}}</custom_ranking_category_attributes>
177
+ <show_cats_not_included_in_navigation>0</show_cats_not_included_in_navigation>
178
  </categories>
179
  <queue>
180
  <active>0</active>
202
  </product_map>
203
  </algoliasearch>
204
  </default>
205
+ </config>
app/code/community/Algolia/Algoliasearch/etc/system.xml CHANGED
@@ -4,7 +4,7 @@
4
  <algoliasearch translate="label" module="algoliasearch">
5
  <label>
6
  <![CDATA[
7
- Algolia Search 1.5.4
8
  <style>
9
  .algoliasearch-admin-menu span {
10
  padding-left: 38px !important;
@@ -32,7 +32,7 @@
32
  <sort_order>10</sort_order>
33
  <show_in_default>1</show_in_default>
34
  <show_in_website>1</show_in_website>
35
- <show_in_store>1</show_in_store>
36
  <expanded>1</expanded>
37
  <comment>
38
  <![CDATA[
@@ -146,7 +146,7 @@
146
  <show_in_store>1</show_in_store>
147
  <comment>
148
  <![CDATA[
149
- If set to Yes, the seach box will display a search-as-you-type drop-down menu. It requires your theme to expose a <code>top.search</code> and <code>content</code> block.
150
  ]]>
151
  </comment>
152
  </is_popup_enabled>
@@ -258,11 +258,25 @@
258
  <show_in_store>1</show_in_store>
259
  <comment>
260
  <![CDATA[
261
- Configure here the pages you don't want to search in.<br><br>
262
- <span style="font-size: 20px; color: #D83900">&#9888;</span> Do not forget to trigger the Algolia Search indexers whenever you modify those settings.
263
  ]]>
264
  </comment>
265
  </excluded_pages>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  </fields>
267
  </autocomplete>
268
  <instant>
@@ -428,6 +442,20 @@
428
  ]]>
429
  </comment>
430
  </show_suggestions_on_no_result_page>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  </fields>
432
  </products>
433
  <categories translate="label">
@@ -474,6 +502,20 @@
474
  ]]>
475
  </comment>
476
  </custom_ranking_category_attributes>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  </fields>
478
  </categories>
479
  <image translate="label">
@@ -503,7 +545,7 @@
503
  <show_in_website>1</show_in_website>
504
  <show_in_store>1</show_in_store>
505
  <comment><![CDATA[You can specify the size of the images used at the Search Results Page.<br />
506
- If your images are already present in some size, eg. 265x265, Algolia's index job may not have to resize those, potentially saving time and resources.]]></comment>
507
  </height>
508
  <type translate="label comment">
509
  <label>Type</label>
@@ -536,7 +578,7 @@
536
  <show_in_store>1</show_in_store>
537
  <comment>
538
  <![CDATA[
539
- If enabled, all inxdexing operations (add, remove & update operations) will be done asynchronously using the CRON mechanism.<br />
540
  <br>
541
  To schedule the run you need to add this in your crontab:<br>
542
  */5 * * * * php -f /absolute/path/to/magento/shell/indexer.php -- -reindex algolia_queue_runner
4
  <algoliasearch translate="label" module="algoliasearch">
5
  <label>
6
  <![CDATA[
7
+ Algolia Search 1.5.5
8
  <style>
9
  .algoliasearch-admin-menu span {
10
  padding-left: 38px !important;
32
  <sort_order>10</sort_order>
33
  <show_in_default>1</show_in_default>
34
  <show_in_website>1</show_in_website>
35
+ <show_in_store>1</show_in_store>
36
  <expanded>1</expanded>
37
  <comment>
38
  <![CDATA[
146
  <show_in_store>1</show_in_store>
147
  <comment>
148
  <![CDATA[
149
+ If set to Yes, the search box will display a search-as-you-type drop-down menu. It requires your theme to expose a <code>top.search</code> and <code>content</code> block.
150
  ]]>
151
  </comment>
152
  </is_popup_enabled>
258
  <show_in_store>1</show_in_store>
259
  <comment>
260
  <![CDATA[
261
+ Configure here the pages you don't want to search in.
 
262
  ]]>
263
  </comment>
264
  </excluded_pages>
265
+ <render_template_directives translate="label comment">
266
+ <label>Render template directives</label>
267
+ <frontend_type>select</frontend_type>
268
+ <source_model>adminhtml/system_config_source_yesno</source_model>
269
+ <sort_order>30</sort_order>
270
+ <show_in_default>1</show_in_default>
271
+ <show_in_website>1</show_in_website>
272
+ <show_in_store>1</show_in_store>
273
+ <comment>
274
+ <![CDATA[
275
+ For CMS pages, template directives (e.g. <code>{{block type="core/template" ...}}</code>) will be processed by default. Set this to "No" if you don't want to have template content indexed.<br><br>
276
+ <span style="font-size: 20px; color: #D83900">&#9888;</span> Do not forget to trigger the Algolia Search indexers whenever you modify those settings.
277
+ ]]>
278
+ </comment>
279
+ </render_template_directives>
280
  </fields>
281
  </autocomplete>
282
  <instant>
442
  ]]>
443
  </comment>
444
  </show_suggestions_on_no_result_page>
445
+ <index_out_of_stock_options translate="label comment">
446
+ <label>Index out of stock options for configurable products</label>
447
+ <frontend_type>select</frontend_type>
448
+ <source_model>adminhtml/system_config_source_yesno</source_model>
449
+ <sort_order>60</sort_order>
450
+ <show_in_default>1</show_in_default>
451
+ <show_in_website>1</show_in_website>
452
+ <show_in_store>1</show_in_store>
453
+ <comment>
454
+ <![CDATA[
455
+ Choose here if you want to index out of stock options for configurable products
456
+ ]]>
457
+ </comment>
458
+ </index_out_of_stock_options>
459
  </fields>
460
  </products>
461
  <categories translate="label">
502
  ]]>
503
  </comment>
504
  </custom_ranking_category_attributes>
505
+ <show_cats_not_included_in_navigation translate="label comment">
506
+ <label>Show categories that are not included in the navigation menu</label>
507
+ <frontend_type>select</frontend_type>
508
+ <source_model>adminhtml/system_config_source_yesno</source_model>
509
+ <sort_order>40</sort_order>
510
+ <show_in_default>1</show_in_default>
511
+ <show_in_website>1</show_in_website>
512
+ <show_in_store>1</show_in_store>
513
+ <comment>
514
+ <![CDATA[
515
+ If set to Yes, Algolia will display all categories in the autocomplete menu ignoring the setting: "Include in navigation menu". Default value: false
516
+ ]]>
517
+ </comment>
518
+ </show_cats_not_included_in_navigation>
519
  </fields>
520
  </categories>
521
  <image translate="label">
545
  <show_in_website>1</show_in_website>
546
  <show_in_store>1</show_in_store>
547
  <comment><![CDATA[You can specify the size of the images used at the Search Results Page.<br />
548
+ If your images are already present in some size, e.g. 265x265, Algolia's index job may not have to resize those, potentially saving time and resources.]]></comment>
549
  </height>
550
  <type translate="label comment">
551
  <label>Type</label>
578
  <show_in_store>1</show_in_store>
579
  <comment>
580
  <![CDATA[
581
+ If enabled, all indexing operations (add, remove & update operations) will be done asynchronously using the CRON mechanism.<br />
582
  <br>
583
  To schedule the run you need to add this in your crontab:<br>
584
  */5 * * * * php -f /absolute/path/to/magento/shell/indexer.php -- -reindex algolia_queue_runner
app/design/frontend/base/default/template/algoliasearch/beforetopsearch.phtml CHANGED
@@ -4,6 +4,7 @@ $config = Mage::helper('algoliasearch/config');
4
  $catalogSearchHelper = $this->helper('catalogsearch'); /** @var $catalogSearchHelper Mage_CatalogSearch_Helper_Data */
5
  $algoliaSearchHelper = $this->helper('algoliasearch'); /** @var $algoliaSearchHelper Algolia_Algoliasearch_Helper_Data */
6
  $product_helper = Mage::helper('algoliasearch/entity_producthelper');
 
7
 
8
  $base_url = Mage::getBaseUrl();
9
 
@@ -12,6 +13,7 @@ $isCategoryPage = false;
12
 
13
  $currency_code = Mage::app()->getStore()->getCurrentCurrencyCode();
14
  $group_id = Mage::getSingleton('customer/session')->getCustomerGroupId();
 
15
  $price_key = $config->isCustomerGroupsEnabled(Mage::app()->getStore()->getStoreId()) ? '.'.$currency_code.'.group_'.$group_id : '.'.$currency_code.'.default';
16
 
17
  $allDepartments = "All departments";
@@ -106,7 +108,7 @@ if ($config->isInstantEnabled() && $isSearchPage) {
106
  },
107
  applicationId: '<?php echo $config->getApplicationID() ?>',
108
  indexName: '<?php echo $product_helper->getBaseIndexName(); ?>',
109
- apiKey: '<?php echo $config->getSearchOnlyAPIKey() ?>',
110
  facets: <?php echo json_encode($config->getFacets()); ?>,
111
  hitsPerPage: <?php echo (int) $config->getNumberOfProductResults() ?>,
112
  sortingIndices: <?php echo json_encode(array_values($config->getSortingIndices())); ?>,
@@ -116,6 +118,7 @@ if ($config->isInstantEnabled() && $isSearchPage) {
116
  priceKey: '<?php echo $price_key; ?>',
117
  currencySymbol: '<?php echo Mage::app()->getLocale()->currency(Mage::app()->getStore()->getCurrentCurrencyCode())->getSymbol(); ?>',
118
  currency_code: '<?php echo $currency_code; ?>',
 
119
  autofocus: true,
120
  request: {
121
  query:<?php echo json_encode(array("value" => html_entity_decode($query))); ?>.value,
@@ -123,6 +126,7 @@ if ($config->isInstantEnabled() && $isSearchPage) {
123
  refinement_value: '<?php echo $refinement_value; ?>',
124
  path: '<?php echo $path; ?>'
125
  },
 
126
  showSuggestionsOnNoResultsPage: <?php echo $config->showSuggestionsOnNoResultsPage() ? "true" : "false"; ?>,
127
  baseUrl: '<?php echo $base_url ?>',
128
  popularQueries: <?php echo json_encode($config->getPopularQueries()); ?>
@@ -211,6 +215,7 @@ if ($config->isInstantEnabled() && $isSearchPage) {
211
  return algoliaBundle.instantsearch.widgets.refinementList({
212
  container: facet.wrapper.appendChild(document.createElement('div')),
213
  attributeName: facet.attribute,
 
214
  operator: 'and',
215
  templates: templates,
216
  cssClasses: {
@@ -225,6 +230,7 @@ if ($config->isInstantEnabled() && $isSearchPage) {
225
  return algoliaBundle.instantsearch.widgets.refinementList({
226
  container: facet.wrapper.appendChild(document.createElement('div')),
227
  attributeName: facet.attribute,
 
228
  operator: 'or',
229
  templates: templates,
230
  cssClasses: {
@@ -301,6 +307,10 @@ if ($config->isInstantEnabled() && $isSearchPage) {
301
  }
302
  else if (section.name === "categories" || section.name === "pages")
303
  {
 
 
 
 
304
  source = {
305
  source: $.fn.autocomplete.sources.hits(algolia_client.initIndex(algoliaConfig.indexName + "_" + section.name), options),
306
  name: i,
4
  $catalogSearchHelper = $this->helper('catalogsearch'); /** @var $catalogSearchHelper Mage_CatalogSearch_Helper_Data */
5
  $algoliaSearchHelper = $this->helper('algoliasearch'); /** @var $algoliaSearchHelper Algolia_Algoliasearch_Helper_Data */
6
  $product_helper = Mage::helper('algoliasearch/entity_producthelper');
7
+ $algolia_helper = Mage::helper('algoliasearch/algoliahelper');
8
 
9
  $base_url = Mage::getBaseUrl();
10
 
13
 
14
  $currency_code = Mage::app()->getStore()->getCurrentCurrencyCode();
15
  $group_id = Mage::getSingleton('customer/session')->getCustomerGroupId();
16
+
17
  $price_key = $config->isCustomerGroupsEnabled(Mage::app()->getStore()->getStoreId()) ? '.'.$currency_code.'.group_'.$group_id : '.'.$currency_code.'.default';
18
 
19
  $allDepartments = "All departments";
108
  },
109
  applicationId: '<?php echo $config->getApplicationID() ?>',
110
  indexName: '<?php echo $product_helper->getBaseIndexName(); ?>',
111
+ apiKey: '<?php echo $algolia_helper->generateSearchSecuredApiKey($config->getSearchOnlyAPIKey(), $config->getAttributesToRetrieve($group_id)) ?>',
112
  facets: <?php echo json_encode($config->getFacets()); ?>,
113
  hitsPerPage: <?php echo (int) $config->getNumberOfProductResults() ?>,
114
  sortingIndices: <?php echo json_encode(array_values($config->getSortingIndices())); ?>,
118
  priceKey: '<?php echo $price_key; ?>',
119
  currencySymbol: '<?php echo Mage::app()->getLocale()->currency(Mage::app()->getStore()->getCurrentCurrencyCode())->getSymbol(); ?>',
120
  currency_code: '<?php echo $currency_code; ?>',
121
+ maxValuesPerFacet: <?php echo (int) $config->getMaxValuesPerFacet(); ?>,
122
  autofocus: true,
123
  request: {
124
  query:<?php echo json_encode(array("value" => html_entity_decode($query))); ?>.value,
126
  refinement_value: '<?php echo $refinement_value; ?>',
127
  path: '<?php echo $path; ?>'
128
  },
129
+ show_cats_not_included_in_navigation: <?php echo $config->showCatsNotIncludedInNavigation() ? "true" : "false"; ?>,
130
  showSuggestionsOnNoResultsPage: <?php echo $config->showSuggestionsOnNoResultsPage() ? "true" : "false"; ?>,
131
  baseUrl: '<?php echo $base_url ?>',
132
  popularQueries: <?php echo json_encode($config->getPopularQueries()); ?>
215
  return algoliaBundle.instantsearch.widgets.refinementList({
216
  container: facet.wrapper.appendChild(document.createElement('div')),
217
  attributeName: facet.attribute,
218
+ limit: algoliaConfig.maxValuesPerFacet,
219
  operator: 'and',
220
  templates: templates,
221
  cssClasses: {
230
  return algoliaBundle.instantsearch.widgets.refinementList({
231
  container: facet.wrapper.appendChild(document.createElement('div')),
232
  attributeName: facet.attribute,
233
+ limit: algoliaConfig.maxValuesPerFacet,
234
  operator: 'or',
235
  templates: templates,
236
  cssClasses: {
307
  }
308
  else if (section.name === "categories" || section.name === "pages")
309
  {
310
+ if (section.name === "categories" && algoliaConfig.show_cats_not_included_in_navigation == false) {
311
+ options.numericFilters = 'include_in_menu=1';
312
+ }
313
+
314
  source = {
315
  source: $.fn.autocomplete.sources.hits(algolia_client.initIndex(algoliaConfig.indexName + "_" + section.name), options),
316
  name: i,
app/design/frontend/base/default/template/algoliasearch/topsearch.phtml CHANGED
@@ -9,7 +9,7 @@
9
  <?php
10
  $config = Mage::helper('algoliasearch/config');
11
  $catalogSearchHelper = $this->helper('catalogsearch');
12
- $group_id = Mage::getSingleton('customer/session')->getCustomer()->getGroupId();
13
  $currency_code = Mage::app()->getStore()->getCurrentCurrencyCode();
14
  $price_key = $config->isCustomerGroupsEnabled(Mage::app()->getStore()->getStoreId()) ? '.'.$currency_code.'.group_'.$group_id : '.'.$currency_code.'.default';
15
 
@@ -635,6 +635,7 @@ $placeholder = $this->__('Search for products, categories, ...');
635
  attributes: hierarchical_levels,
636
  separator: ' /// ',
637
  alwaysGetRootLevel: true,
 
638
  templates: templates,
639
  sortBy: ['name:asc'],
640
  cssClasses: {
9
  <?php
10
  $config = Mage::helper('algoliasearch/config');
11
  $catalogSearchHelper = $this->helper('catalogsearch');
12
+ $group_id = Mage::getSingleton('customer/session')->getCustomerGroupId();
13
  $currency_code = Mage::app()->getStore()->getCurrentCurrencyCode();
14
  $price_key = $config->isCustomerGroupsEnabled(Mage::app()->getStore()->getStoreId()) ? '.'.$currency_code.'.group_'.$group_id : '.'.$currency_code.'.default';
15
 
635
  attributes: hierarchical_levels,
636
  separator: ' /// ',
637
  alwaysGetRootLevel: true,
638
+ limit: algoliaConfig.maxValuesPerFacet,
639
  templates: templates,
640
  sortBy: ['name:asc'],
641
  cssClasses: {
app/etc/modules/Algolia_Algoliasearch.xml CHANGED
@@ -4,7 +4,7 @@
4
  <Algolia_Algoliasearch>
5
  <active>true</active>
6
  <codePool>community</codePool>
7
- <version>1.5.4</version>
8
  </Algolia_Algoliasearch>
9
  </modules>
10
  </config>
4
  <Algolia_Algoliasearch>
5
  <active>true</active>
6
  <codePool>community</codePool>
7
+ <version>1.5.5</version>
8
  </Algolia_Algoliasearch>
9
  </modules>
10
  </config>
lib/AlgoliaSearch/AlgoliaException.php CHANGED
@@ -1,4 +1,5 @@
1
  <?php
 
2
  /*
3
  * Copyright (c) 2013 Algolia
4
  * http://www.algolia.com/
@@ -23,8 +24,9 @@
23
  *
24
  *
25
  */
 
26
  namespace AlgoliaSearch;
27
 
28
  class AlgoliaException extends \Exception
29
  {
30
- }
1
  <?php
2
+
3
  /*
4
  * Copyright (c) 2013 Algolia
5
  * http://www.algolia.com/
24
  *
25
  *
26
  */
27
+
28
  namespace AlgoliaSearch;
29
 
30
  class AlgoliaException extends \Exception
31
  {
32
+ }
lib/AlgoliaSearch/Client.php CHANGED
@@ -1,4 +1,5 @@
1
  <?php
 
2
  /*
3
  * Copyright (c) 2013 Algolia
4
  * http://www.algolia.com/
@@ -23,109 +24,215 @@
23
  *
24
  *
25
  */
 
26
  namespace AlgoliaSearch;
27
 
28
  /**
29
  * Entry point in the PHP API.
30
  * You should instantiate a Client object with your ApplicationID, ApiKey and Hosts
31
- * to start using Algolia Search API
32
  */
33
- class Client {
 
 
 
 
34
 
 
 
 
35
  protected $context;
36
- protected $cainfoPath;
37
 
38
- /*
39
- * Algolia Search initialization
40
- * @param applicationID the application ID you have in your admin interface
41
- * @param apiKey a valid API key for the service
42
- * @param hostsArray the list of hosts that you have received for the service
43
  */
44
- function __construct($applicationID, $apiKey, $hostsArray = null, $options = array()) {
45
- if ($hostsArray == null) {
46
- $this->context = new ClientContext($applicationID, $apiKey, null);
47
- } else {
48
- $this->context = new ClientContext($applicationID, $apiKey, $hostsArray);
49
- }
50
- if(!function_exists('curl_init')){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  throw new \Exception('AlgoliaSearch requires the CURL PHP extension.');
52
  }
53
- if(!function_exists('json_decode')){
54
  throw new \Exception('AlgoliaSearch requires the JSON PHP extension.');
55
  }
56
- $this->cainfoPath = __DIR__ . '/resources/ca-bundle.crt';
 
57
  foreach ($options as $option => $value) {
58
- if ($option == "cainfo") {
59
- $this->cainfoPath = $value;
60
- } else {
61
- throw new \Exception('Unknown option: ' . $option);
 
 
 
 
 
 
 
 
62
  }
63
  }
 
 
64
  }
65
 
66
- /*
67
- * Release curl handle
68
  */
69
- function __destruct() {
 
70
  }
71
 
72
- /*
73
- * Change the default connect timeout of 2s to a custom value (only useful if your server has a very slow connectivity to Algolia backend)
74
- * @param connectTimeout the connection timeout
75
- * @param timeout the read timeout for the query
76
- * @param searchTimeout the read timeout used for search queries only
 
 
 
 
77
  */
78
- public function setConnectTimeout($connectTimeout, $timeout = 30, $searchTimeout = 5) {
 
79
  $version = curl_version();
80
- if ((version_compare(phpversion(), '5.2.3', '<') || version_compare($version['version'], '7.16.2', '<')) && $this->context->connectTimeout < 1) {
81
- throw new AlgoliaException("The timeout can't be a float with a PHP version less than 5.2.3 or a curl version less than 7.16.2");
 
 
 
 
 
82
  }
83
  $this->context->connectTimeout = $connectTimeout;
84
  $this->context->readTimeout = $timeout;
85
  $this->context->searchTimeout = $searchTimeout;
86
  }
87
 
88
- /*
89
  * Allow to use IP rate limit when you have a proxy between end-user and Algolia.
90
- * This option will set the X-Forwarded-For HTTP header with the client IP and the X-Forwarded-API-Key with the API Key having rate limits.
91
- * @param adminAPIKey the admin API Key you can find in your dashboard
92
- * @param endUserIP the end user IP (you can use both IPV4 or IPV6 syntax)
93
- * @param rateLimitAPIKey the API key on which you have a rate limit
 
 
94
  */
95
- public function enableRateLimitForward($adminAPIKey, $endUserIP, $rateLimitAPIKey) {
 
96
  $this->context->setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey);
97
  }
98
 
99
- /*
100
- * Disable IP rate limit enabled with enableRateLimitForward() function
 
 
 
 
 
 
 
 
 
101
  */
102
- public function disableRateLimitForward() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  $this->context->disableRateLimit();
104
  }
105
 
106
- /*
107
- * Call isAlive
108
  */
109
- public function isAlive() {
110
- $this->request($this->context, "GET", "/1/isalive", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
111
  }
112
 
113
- /*
114
- * Allow to set custom headers
 
 
 
115
  */
116
- public function setExtraHeader($key, $value) {
 
117
  $this->context->setExtraHeader($key, $value);
118
  }
119
 
120
- /*
121
- * This method allows to query multiple indexes with one API call
 
 
 
 
122
  *
 
 
 
 
123
  */
124
- public function multipleQueries($queries, $indexNameKey = "indexName", $strategy = "none") {
 
125
  if ($queries == null) {
126
  throw new \Exception('No query provided');
127
  }
128
- $requests = array();
129
  foreach ($queries as $query) {
130
  if (array_key_exists($indexNameKey, $query)) {
131
  $indexes = $query[$indexNameKey];
@@ -133,245 +240,493 @@ class Client {
133
  } else {
134
  throw new \Exception('indexName is mandatory');
135
  }
136
- foreach ($query as $key => $value) {
137
- if (gettype($value) == "array") {
138
- $query[$key] = json_encode($value);
139
- }
140
- }
141
- $req = array("indexName" => $indexes, "params" => http_build_query($query));
142
  array_push($requests, $req);
143
  }
144
- return $this->request($this->context, "POST", "/1/indexes/*/queries?strategy=" . $strategy, array(), array("requests" => $requests), $this->context->readHostsArray, $this->context->connectTimeout, $this->context->searchTimeout);
 
 
 
 
 
 
 
 
 
 
145
  }
146
 
147
- /*
148
  * List all existing indexes
149
  * return an object in the form:
150
- * array("items" => array(
151
- * array("name" => "contacts", "createdAt" => "2013-01-18T15:33:13.556Z"),
152
- * array("name" => "notes", "createdAt" => "2013-01-18T15:33:13.556Z")
153
- * ))
 
 
 
 
 
 
154
  */
155
- public function listIndexes() {
156
- return $this->request($this->context, "GET", "/1/indexes/", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
157
  }
158
 
159
- /*
160
- * Delete an index
161
  *
162
- * @param indexName the name of index to delete
163
- * return an object containing a "deletedAt" attribute
 
164
  */
165
- public function deleteIndex($indexName) {
166
- return $this->request($this->context, "DELETE", "/1/indexes/" . urlencode($indexName), null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
167
  }
168
 
169
  /**
170
  * Move an existing index.
171
- * @param srcIndexName the name of index to copy.
172
- * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
 
 
 
 
173
  */
174
- public function moveIndex($srcIndexName, $dstIndexName) {
175
- $request = array("operation" => "move", "destination" => $dstIndexName);
176
- return $this->request($this->context, "POST", "/1/indexes/" . urlencode($srcIndexName) . "/operation", array(), $request, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
 
179
  /**
180
  * Copy an existing index.
181
- * @param srcIndexName the name of index to copy.
182
- * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
 
 
 
 
183
  */
184
- public function copyIndex($srcIndexName, $dstIndexName) {
185
- $request = array("operation" => "copy", "destination" => $dstIndexName);
186
- return $this->request($this->context, "POST", "/1/indexes/" . urlencode($srcIndexName) . "/operation", array(), $request, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
 
187
  }
188
 
189
  /**
190
  * Return last logs entries.
191
- * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
192
- * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
 
 
 
 
 
 
193
  */
194
- public function getLogs($offset = 0, $length = 10, $type = "all") {
195
- if (gettype($type) == "boolean") { //Old prototype onlyError
 
196
  if ($type) {
197
- $type = "error";
198
  } else {
199
- $type = "all";
200
  }
201
  }
202
- return $this->request($this->context, "GET", "/1/logs?offset=" . $offset . "&length=" . $length . "&type=" . $type, null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
203
- }
204
 
205
- /*
206
- * Get the index object initialized (no server call needed for initialization)
 
 
 
 
 
 
 
 
 
207
 
208
- * @param indexName the name of index
 
 
 
 
 
 
 
209
  */
210
- public function initIndex($indexName) {
 
211
  if (empty($indexName)) {
212
  throw new AlgoliaException('Invalid index name: empty string');
213
  }
 
214
  return new Index($this->context, $this, $indexName);
215
  }
216
 
217
- /*
218
- * List all existing user keys with their associated ACLs
 
 
219
  *
 
220
  */
221
- public function listUserKeys() {
222
- return $this->request($this->context, "GET", "/1/keys", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
223
  }
224
 
225
- /*
226
- * Get ACL of a user key
227
  *
 
 
 
228
  */
229
- public function getUserKeyACL($key) {
230
- return $this->request($this->context, "GET", "/1/keys/" . $key, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
231
  }
232
 
233
- /*
234
- * Delete an existing user key
 
 
235
  *
 
236
  */
237
- public function deleteUserKey($key) {
238
- return $this->request($this->context, "DELETE", "/1/keys/" . $key, null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
239
  }
240
 
241
- /*
242
- * Create a new user key
243
- *
244
- * @param obj can be two different parameters:
245
- * The list of parameters for this key. Defined by a NSDictionary that
246
- * can contains the following values:
247
- * - acl: array of string
248
- * - indices: array of string
249
- * - validity: int
250
- * - referers: array of string
251
- * - description: string
252
- * - maxHitsPerQuery: integer
253
- * - queryParameters: string
254
- * - maxQueriesPerIPPerHour: integer
255
- * Or the list of ACL for this key. Defined by an array of NSString that
256
- * can contains the following values:
257
- * - search: allow to search (https and http)
258
- * - addObject: allows to add/update an object in the index (https only)
259
- * - deleteObject : allows to delete an existing object (https only)
260
- * - deleteIndex : allows to delete index content (https only)
261
- * - settings : allows to get index settings (https only)
262
- * - editSettings : allows to change index settings (https only)
263
- * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
264
- * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit).
265
- * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited)
266
- * @param indexes Specify the list of indices to target (null means all)
267
- */
268
- public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) {
 
 
 
 
 
 
 
 
269
  if ($obj !== array_values($obj)) { // is dict of value
270
  $params = $obj;
271
- $params["validity"] = $validity;
272
- $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour;
273
- $params["maxHitsPerQuery"] = $maxHitsPerQuery;
274
  } else {
275
- $params = array(
276
- "acl" => $obj,
277
- "validity" => $validity,
278
- "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour,
279
- "maxHitsPerQuery" => $maxHitsPerQuery
280
- );
281
  }
282
 
283
  if ($indexes != null) {
284
  $params['indexes'] = $indexes;
285
  }
286
- return $this->request($this->context, "POST", "/1/keys", array(), $params, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
287
  }
288
 
289
- /*
290
- * Update a user key
291
- *
292
- * @param obj can be two different parameters:
293
- * The list of parameters for this key. Defined by a NSDictionary that
294
- * can contains the following values:
295
- * - acl: array of string
296
- * - indices: array of string
297
- * - validity: int
298
- * - referers: array of string
299
- * - description: string
300
- * - maxHitsPerQuery: integer
301
- * - queryParameters: string
302
- * - maxQueriesPerIPPerHour: integer
303
- * Or the list of ACL for this key. Defined by an array of NSString that
304
- * can contains the following values:
305
- * - search: allow to search (https and http)
306
- * - addObject: allows to add/update an object in the index (https only)
307
- * - deleteObject : allows to delete an existing object (https only)
308
- * - deleteIndex : allows to delete index content (https only)
309
- * - settings : allows to get index settings (https only)
310
- * - editSettings : allows to change index settings (https only)
311
- * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
312
- * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit).
313
- * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited)
314
- * @param indexes Specify the list of indices to target (null means all)
315
- */
316
- public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  if ($obj !== array_values($obj)) { // is dict of value
318
  $params = $obj;
319
- $params["validity"] = $validity;
320
- $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour;
321
- $params["maxHitsPerQuery"] = $maxHitsPerQuery;
322
  } else {
323
- $params = array(
324
- "acl" => $obj,
325
- "validity" => $validity,
326
- "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour,
327
- "maxHitsPerQuery" => $maxHitsPerQuery
328
- );
329
  }
330
  if ($indexes != null) {
331
  $params['indexes'] = $indexes;
332
  }
333
- return $this->request($this->context, "PUT", "/1/keys/" . $key, array(), $params, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
334
  }
335
 
336
  /**
337
- * Send a batch request targeting multiple indices
338
- * @param $requests an associative array defining the batch request body
 
 
 
339
  */
340
- public function batch($requests) {
341
- return $this->request($this->context, "POST", "/1/indexes/*/batch", array(), array("requests" => $requests),
342
- $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
343
  }
344
 
345
- /*
346
- * Generate a secured and public API Key from a list of tagFilters and an
347
- * optional user token identifying the current user
348
  *
349
- * @param privateApiKey your private API Key
350
- * @param tagFilters the list of tags applied to the query (used as security)
351
- * @param userToken an optional token identifying the current user
352
  *
 
353
  */
354
- public function generateSecuredApiKey($privateApiKey, $tagFilters, $userToken = null) {
355
- if (is_array($tagFilters)) {
356
- $tmp = array();
357
- foreach ($tagFilters as $tag) {
358
- if (is_array($tag)) {
359
- $tmp2 = array();
360
- foreach ($tag as $tag2) {
361
- array_push($tmp2, $tag2);
 
 
 
 
 
 
 
 
362
  }
363
- array_push($tmp, '(' . join(',', $tmp2) . ')');
364
- } else {
365
- array_push($tmp, $tag);
366
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
368
- $tagFilters = join(',', $tmp);
369
  }
370
- return hash_hmac('sha256', $tagFilters . $userToken, $privateApiKey);
 
371
  }
372
 
373
- public function request($context, $method, $path, $params = array(), $data = array(), $hostsArray, $connectTimeout, $readTimeout) {
374
- $exceptions = array();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  $cnt = 0;
376
  foreach ($hostsArray as &$host) {
377
  $cnt += 1;
@@ -381,25 +736,51 @@ class Client {
381
  }
382
  try {
383
  $res = $this->doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout);
384
- if ($res !== null)
385
  return $res;
 
386
  } catch (AlgoliaException $e) {
387
  throw $e;
388
  } catch (\Exception $e) {
389
  $exceptions[$host] = $e->getMessage();
390
  }
391
  }
392
- throw new AlgoliaException('Hosts unreachable: ' . join(",", $exceptions));
393
  }
394
 
395
- public function doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout) {
396
- if (strpos($host, "http") === 0) {
397
- $url = $host . $path;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  } else {
399
- $url = "https://" . $host . $path;
400
  }
 
401
  if ($params != null && count($params) > 0) {
402
- $params2 = array();
403
  foreach ($params as $key => $val) {
404
  if (is_array($val)) {
405
  $params2[$key] = json_encode($val);
@@ -407,35 +788,47 @@ class Client {
407
  $params2[$key] = $val;
408
  }
409
  }
410
- $url .= "?" . http_build_query($params2);
411
-
412
  }
 
413
  // initialize curl library
414
  $curlHandle = curl_init();
 
 
 
 
 
 
 
 
 
 
415
  //curl_setopt($curlHandle, CURLOPT_VERBOSE, true);
416
  if ($context->adminAPIKey == null) {
417
- curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge(array(
418
- 'X-Algolia-Application-Id: ' . $context->applicationID,
419
- 'X-Algolia-API-Key: ' . $context->apiKey,
420
- 'Content-type: application/json'
421
- ), $context->headers));
422
  } else {
423
- curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge(array(
424
- 'X-Algolia-Application-Id: ' . $context->applicationID,
425
- 'X-Algolia-API-Key: ' . $context->adminAPIKey,
426
- 'X-Forwarded-For: ' . $context->endUserIP,
427
- 'X-Forwarded-API-Key: ' . $context->rateLimitAPIKey,
428
- 'Content-type: application/json'
429
- ), $context->headers));
430
- }
431
- curl_setopt($curlHandle, CURLOPT_USERAGENT, "Algolia for PHP " . Version::get());
 
 
432
  //Return the output instead of printing it
433
  curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
434
  curl_setopt($curlHandle, CURLOPT_FAILONERROR, true);
435
  curl_setopt($curlHandle, CURLOPT_ENCODING, '');
436
  curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
437
  curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
438
- curl_setopt($curlHandle, CURLOPT_CAINFO, $this->cainfoPath);
439
 
440
  curl_setopt($curlHandle, CURLOPT_URL, $url);
441
  $version = curl_version();
@@ -447,26 +840,30 @@ class Client {
447
  curl_setopt($curlHandle, CURLOPT_TIMEOUT, $readTimeout);
448
  }
449
 
450
- curl_setopt($curlHandle, CURLOPT_NOSIGNAL, 1); # The problem is that on (Li|U)nix, when libcurl uses the standard name resolver, a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm.
 
 
451
  curl_setopt($curlHandle, CURLOPT_FAILONERROR, false);
452
 
453
  if ($method === 'GET') {
454
  curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'GET');
455
  curl_setopt($curlHandle, CURLOPT_HTTPGET, true);
456
  curl_setopt($curlHandle, CURLOPT_POST, false);
457
- } else if ($method === 'POST') {
458
- $body = ($data) ? json_encode($data) : '';
459
- curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
460
- curl_setopt($curlHandle, CURLOPT_POST, true);
461
- curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
462
- } elseif ($method === 'DELETE') {
463
- curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE');
464
- curl_setopt($curlHandle, CURLOPT_POST, false);
465
- } elseif ($method === 'PUT') {
466
- $body = ($data) ? json_encode($data) : '';
467
- curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT');
468
- curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
469
- curl_setopt($curlHandle, CURLOPT_POST, true);
 
 
470
  }
471
  $mhandle = $context->getMHandle($curlHandle);
472
 
@@ -475,6 +872,7 @@ class Client {
475
  do {
476
  $mrc = curl_multi_exec($mhandle, $running);
477
  } while ($mrc == CURLM_CALL_MULTI_PERFORM);
 
478
  while ($running && $mrc == CURLM_OK) {
479
  if (curl_multi_select($mhandle, 0.1) == -1) {
480
  usleep(100);
@@ -483,17 +881,21 @@ class Client {
483
  $mrc = curl_multi_exec($mhandle, $running);
484
  } while ($mrc == CURLM_CALL_MULTI_PERFORM);
485
  }
486
- $http_status = (int)curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
 
487
  $response = curl_multi_getcontent($curlHandle);
488
  $error = curl_error($curlHandle);
 
489
  if (!empty($error)) {
490
  throw new \Exception($error);
491
  }
 
492
  if ($http_status === 0 || $http_status === 503) {
493
  // Could not reach host or service unavailable, try with another one if we have it
494
  $context->releaseMHandle($curlHandle);
495
  curl_close($curlHandle);
496
- return null;
 
497
  }
498
 
499
  $answer = json_decode($response, true);
@@ -501,10 +903,9 @@ class Client {
501
  curl_close($curlHandle);
502
 
503
  if (intval($http_status / 100) == 4) {
504
- throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status + " error");
505
- }
506
- elseif (intval($http_status / 100) != 2) {
507
- throw new \Exception($http_status . ": " . $response);
508
  }
509
 
510
  switch (json_last_error()) {
@@ -520,7 +921,8 @@ class Client {
520
  case JSON_ERROR_STATE_MISMATCH:
521
  $errorMsg = 'JSON parsing error: underflow or the modes mismatch';
522
  break;
523
- case (defined('JSON_ERROR_UTF8') ? JSON_ERROR_UTF8 : -1): // PHP 5.3 less than 1.2.2 (Ubuntu 10.04 LTS)
 
524
  $errorMsg = 'JSON parsing error: malformed UTF-8 characters, possibly incorrectly encoded';
525
  break;
526
  case JSON_ERROR_NONE:
@@ -528,10 +930,112 @@ class Client {
528
  $errorMsg = null;
529
  break;
530
  }
531
- if ($errorMsg !== null)
532
  throw new AlgoliaException($errorMsg);
 
533
 
534
  return $answer;
535
  }
536
- }
537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <?php
2
+
3
  /*
4
  * Copyright (c) 2013 Algolia
5
  * http://www.algolia.com/
24
  *
25
  *
26
  */
27
+
28
  namespace AlgoliaSearch;
29
 
30
  /**
31
  * Entry point in the PHP API.
32
  * You should instantiate a Client object with your ApplicationID, ApiKey and Hosts
33
+ * to start using Algolia Search API.
34
  */
35
+ class Client
36
+ {
37
+ const CAINFO = 'cainfo';
38
+ const CURLOPT = 'curloptions';
39
+ const PLACES_ENABLED = 'placesEnabled';
40
 
41
+ /**
42
+ * @var ClientContext
43
+ */
44
  protected $context;
 
45
 
46
+ /**
47
+ * @var string
 
 
 
48
  */
49
+ protected $caInfoPath;
50
+
51
+ /**
52
+ * @var array
53
+ */
54
+ protected $curlConstants;
55
+
56
+ /**
57
+ * @var array
58
+ */
59
+ protected $curlOptions = [];
60
+
61
+ /**
62
+ * @var bool
63
+ */
64
+ protected $placesEnabled = false;
65
+
66
+ /**
67
+ * Algolia Search initialization.
68
+ *
69
+ * @param string $applicationID the application ID you have in your admin interface
70
+ * @param string $apiKey a valid API key for the service
71
+ * @param array|null $hostsArray the list of hosts that you have received for the service
72
+ * @param array $options
73
+ *
74
+ * @throws \Exception
75
+ */
76
+ public function __construct($applicationID, $apiKey, $hostsArray = null, $options = [])
77
+ {
78
+ if (!function_exists('curl_init')) {
79
  throw new \Exception('AlgoliaSearch requires the CURL PHP extension.');
80
  }
81
+ if (!function_exists('json_decode')) {
82
  throw new \Exception('AlgoliaSearch requires the JSON PHP extension.');
83
  }
84
+
85
+ $this->caInfoPath = __DIR__.'/resources/ca-bundle.crt';
86
  foreach ($options as $option => $value) {
87
+ switch ($option) {
88
+ case self::CAINFO:
89
+ $this->caInfoPath = $value;
90
+ break;
91
+ case self::CURLOPT:
92
+ $this->curlOptions = $this->checkCurlOptions($value);
93
+ break;
94
+ case self::PLACES_ENABLED:
95
+ $this->placesEnabled = (bool) $value;
96
+ break;
97
+ default:
98
+ throw new \Exception('Unknown option: '.$option);
99
  }
100
  }
101
+
102
+ $this->context = new ClientContext($applicationID, $apiKey, $hostsArray, $this->placesEnabled);
103
  }
104
 
105
+ /**
106
+ * Release curl handle.
107
  */
108
+ public function __destruct()
109
+ {
110
  }
111
 
112
+ /**
113
+ * Change the default connect timeout of 2s to a custom value
114
+ * (only useful if your server has a very slow connectivity to Algolia backend).
115
+ *
116
+ * @param int $connectTimeout the connection timeout
117
+ * @param int $timeout the read timeout for the query
118
+ * @param int $searchTimeout the read timeout used for search queries only
119
+ *
120
+ * @throws AlgoliaException
121
  */
122
+ public function setConnectTimeout($connectTimeout, $timeout = 30, $searchTimeout = 5)
123
+ {
124
  $version = curl_version();
125
+ $isPhpOld = version_compare(phpversion(), '5.2.3', '<');
126
+ $isCurlOld = version_compare($version['version'], '7.16.2', '<');
127
+
128
+ if (($isPhpOld || $isCurlOld) && $this->context->connectTimeout < 1) {
129
+ throw new AlgoliaException(
130
+ "The timeout can't be a float with a PHP version less than 5.2.3 or a curl version less than 7.16.2"
131
+ );
132
  }
133
  $this->context->connectTimeout = $connectTimeout;
134
  $this->context->readTimeout = $timeout;
135
  $this->context->searchTimeout = $searchTimeout;
136
  }
137
 
138
+ /**
139
  * Allow to use IP rate limit when you have a proxy between end-user and Algolia.
140
+ * This option will set the X-Forwarded-For HTTP header with the client IP
141
+ * and the X-Forwarded-API-Key with the API Key having rate limits.
142
+ *
143
+ * @param string $adminAPIKey the admin API Key you can find in your dashboard
144
+ * @param string $endUserIP the end user IP (you can use both IPV4 or IPV6 syntax)
145
+ * @param string $rateLimitAPIKey the API key on which you have a rate limit
146
  */
147
+ public function enableRateLimitForward($adminAPIKey, $endUserIP, $rateLimitAPIKey)
148
+ {
149
  $this->context->setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey);
150
  }
151
 
152
+ /**
153
+ * The aggregation of the queries to retrieve the latest query
154
+ * uses the IP or the user token to work efficiently.
155
+ * If the queries are made from your backend server,
156
+ * the IP will be the same for all of the queries.
157
+ * We're supporting the following HTTP header to forward the IP of your end-user
158
+ * to the engine, you just need to set it for each query.
159
+ *
160
+ * @see https://www.algolia.com/doc/faq/analytics/will-the-analytics-still-work-if-i-perform-the-search-through-my-backend
161
+ *
162
+ * @param string $ip
163
  */
164
+ public function setForwardedFor($ip)
165
+ {
166
+ $this->context->setForwardedFor($ip);
167
+ }
168
+
169
+ /**
170
+ * It's possible to use the following token to track users that have the same IP
171
+ * or to track users that use different devices.
172
+ *
173
+ * @see https://www.algolia.com/doc/faq/analytics/will-the-analytics-still-work-if-i-perform-the-search-through-my-backend
174
+ *
175
+ * @param string $token
176
+ */
177
+ public function setAlgoliaUserToken($token)
178
+ {
179
+ $this->context->setAlgoliaUserToken($token);
180
+ }
181
+
182
+ /**
183
+ * Disable IP rate limit enabled with enableRateLimitForward() function.
184
+ */
185
+ public function disableRateLimitForward()
186
+ {
187
  $this->context->disableRateLimit();
188
  }
189
 
190
+ /**
191
+ * Call isAlive.
192
  */
193
+ public function isAlive()
194
+ {
195
+ $this->request(
196
+ $this->context,
197
+ 'GET',
198
+ '/1/isalive',
199
+ null,
200
+ null,
201
+ $this->context->readHostsArray,
202
+ $this->context->connectTimeout,
203
+ $this->context->readTimeout
204
+ );
205
  }
206
 
207
+ /**
208
+ * Allow to set custom headers.
209
+ *
210
+ * @param string $key
211
+ * @param string $value
212
  */
213
+ public function setExtraHeader($key, $value)
214
+ {
215
  $this->context->setExtraHeader($key, $value);
216
  }
217
 
218
+ /**
219
+ * This method allows to query multiple indexes with one API call.
220
+ *
221
+ * @param array $queries
222
+ * @param string $indexNameKey
223
+ * @param string $strategy
224
  *
225
+ * @return mixed
226
+ *
227
+ * @throws AlgoliaException
228
+ * @throws \Exception
229
  */
230
+ public function multipleQueries($queries, $indexNameKey = 'indexName', $strategy = 'none')
231
+ {
232
  if ($queries == null) {
233
  throw new \Exception('No query provided');
234
  }
235
+ $requests = [];
236
  foreach ($queries as $query) {
237
  if (array_key_exists($indexNameKey, $query)) {
238
  $indexes = $query[$indexNameKey];
240
  } else {
241
  throw new \Exception('indexName is mandatory');
242
  }
243
+ $req = ['indexName' => $indexes, 'params' => $this->buildQuery($query)];
244
+
 
 
 
 
245
  array_push($requests, $req);
246
  }
247
+
248
+ return $this->request(
249
+ $this->context,
250
+ 'POST',
251
+ '/1/indexes/*/queries?strategy='.$strategy,
252
+ [],
253
+ ['requests' => $requests],
254
+ $this->context->readHostsArray,
255
+ $this->context->connectTimeout,
256
+ $this->context->searchTimeout
257
+ );
258
  }
259
 
260
+ /**
261
  * List all existing indexes
262
  * return an object in the form:
263
+ * array(
264
+ * "items" => array(
265
+ * array("name" => "contacts", "createdAt" => "2013-01-18T15:33:13.556Z"),
266
+ * array("name" => "notes", "createdAt" => "2013-01-18T15:33:13.556Z")
267
+ * )
268
+ * ).
269
+ *
270
+ * @return mixed
271
+ *
272
+ * @throws AlgoliaException
273
  */
274
+ public function listIndexes()
275
+ {
276
+ return $this->request(
277
+ $this->context,
278
+ 'GET',
279
+ '/1/indexes/',
280
+ null,
281
+ null,
282
+ $this->context->readHostsArray,
283
+ $this->context->connectTimeout,
284
+ $this->context->readTimeout
285
+ );
286
  }
287
 
288
+ /**
289
+ * Delete an index.
290
  *
291
+ * @param string $indexName the name of index to delete
292
+ *
293
+ * @return mixed an object containing a "deletedAt" attribute
294
  */
295
+ public function deleteIndex($indexName)
296
+ {
297
+ return $this->request(
298
+ $this->context,
299
+ 'DELETE',
300
+ '/1/indexes/'.urlencode($indexName),
301
+ null,
302
+ null,
303
+ $this->context->writeHostsArray,
304
+ $this->context->connectTimeout,
305
+ $this->context->readTimeout
306
+ );
307
  }
308
 
309
  /**
310
  * Move an existing index.
311
+ *
312
+ * @param string $srcIndexName the name of index to copy.
313
+ * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten
314
+ * if it already exist).
315
+ *
316
+ * @return mixed
317
  */
318
+ public function moveIndex($srcIndexName, $dstIndexName)
319
+ {
320
+ $request = ['operation' => 'move', 'destination' => $dstIndexName];
321
+
322
+ return $this->request(
323
+ $this->context,
324
+ 'POST',
325
+ '/1/indexes/'.urlencode($srcIndexName).'/operation',
326
+ [],
327
+ $request,
328
+ $this->context->writeHostsArray,
329
+ $this->context->connectTimeout,
330
+ $this->context->readTimeout
331
+ );
332
  }
333
 
334
  /**
335
  * Copy an existing index.
336
+ *
337
+ * @param string $srcIndexName the name of index to copy.
338
+ * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten
339
+ * if it already exist).
340
+ *
341
+ * @return mixed
342
  */
343
+ public function copyIndex($srcIndexName, $dstIndexName)
344
+ {
345
+ $request = ['operation' => 'copy', 'destination' => $dstIndexName];
346
+
347
+ return $this->request(
348
+ $this->context,
349
+ 'POST',
350
+ '/1/indexes/'.urlencode($srcIndexName).'/operation',
351
+ [],
352
+ $request,
353
+ $this->context->writeHostsArray,
354
+ $this->context->connectTimeout,
355
+ $this->context->readTimeout
356
+ );
357
  }
358
 
359
  /**
360
  * Return last logs entries.
361
+ *
362
+ * @param int $offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
363
+ * @param int $length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
364
+ * @param mixed $type
365
+ *
366
+ * @return mixed
367
+ *
368
+ * @throws AlgoliaException
369
  */
370
+ public function getLogs($offset = 0, $length = 10, $type = 'all')
371
+ {
372
+ if (gettype($type) == 'boolean') { //Old prototype onlyError
373
  if ($type) {
374
+ $type = 'error';
375
  } else {
376
+ $type = 'all';
377
  }
378
  }
 
 
379
 
380
+ return $this->request(
381
+ $this->context,
382
+ 'GET',
383
+ '/1/logs?offset='.$offset.'&length='.$length.'&type='.$type,
384
+ null,
385
+ null,
386
+ $this->context->writeHostsArray,
387
+ $this->context->connectTimeout,
388
+ $this->context->readTimeout
389
+ );
390
+ }
391
 
392
+ /**
393
+ * Get the index object initialized (no server call needed for initialization).
394
+ *
395
+ * @param string $indexName the name of index
396
+ *
397
+ * @return Index
398
+ *
399
+ * @throws AlgoliaException
400
  */
401
+ public function initIndex($indexName)
402
+ {
403
  if (empty($indexName)) {
404
  throw new AlgoliaException('Invalid index name: empty string');
405
  }
406
+
407
  return new Index($this->context, $this, $indexName);
408
  }
409
 
410
+ /**
411
+ * List all existing user keys with their associated ACLs.
412
+ *
413
+ * @return mixed
414
  *
415
+ * @throws AlgoliaException
416
  */
417
+ public function listUserKeys()
418
+ {
419
+ return $this->request(
420
+ $this->context,
421
+ 'GET',
422
+ '/1/keys',
423
+ null,
424
+ null,
425
+ $this->context->readHostsArray,
426
+ $this->context->connectTimeout,
427
+ $this->context->readTimeout
428
+ );
429
  }
430
 
431
+ /**
432
+ * Get ACL of a user key.
433
  *
434
+ * @param string $key
435
+ *
436
+ * @return mixed
437
  */
438
+ public function getUserKeyACL($key)
439
+ {
440
+ return $this->request(
441
+ $this->context,
442
+ 'GET',
443
+ '/1/keys/'.$key,
444
+ null,
445
+ null,
446
+ $this->context->readHostsArray,
447
+ $this->context->connectTimeout,
448
+ $this->context->readTimeout
449
+ );
450
  }
451
 
452
+ /**
453
+ * Delete an existing user key.
454
+ *
455
+ * @param string $key
456
  *
457
+ * @return mixed
458
  */
459
+ public function deleteUserKey($key)
460
+ {
461
+ return $this->request(
462
+ $this->context,
463
+ 'DELETE',
464
+ '/1/keys/'.$key,
465
+ null,
466
+ null,
467
+ $this->context->writeHostsArray,
468
+ $this->context->connectTimeout,
469
+ $this->context->readTimeout
470
+ );
471
  }
472
 
473
+ /**
474
+ * Create a new user key.
475
+ *
476
+ * @param $obj can be two different parameters:
477
+ * The list of parameters for this key. Defined by an array that
478
+ * can contain the following values:
479
+ * - acl: array of string
480
+ * - indices: array of string
481
+ * - validity: int
482
+ * - referrers: array of string
483
+ * - description: string
484
+ * - maxHitsPerQuery: integer
485
+ * - queryParameters: string
486
+ * - maxQueriesPerIPPerHour: integer
487
+ * Or the list of ACL for this key. Defined by an array of NSString that
488
+ * can contains the following values:
489
+ * - search: allow to search (https and http)
490
+ * - addObject: allows to add/update an object in the index (https only)
491
+ * - deleteObject : allows to delete an existing object (https only)
492
+ * - deleteIndex : allows to delete index content (https only)
493
+ * - settings : allows to get index settings (https only)
494
+ * - editSettings : allows to change index settings (https only)
495
+ * @param int $validity the number of seconds after which the key will be automatically removed (0 means
496
+ * no time limit for this key)
497
+ * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
498
+ * Defaults to 0 (no rate limit).
499
+ * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
500
+ * Defaults to 0 (unlimited)
501
+ * @param array|null $indexes Specify the list of indices to target (null means all)
502
+ *
503
+ * @return mixed
504
+ *
505
+ * @throws AlgoliaException
506
+ */
507
+ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null)
508
+ {
509
  if ($obj !== array_values($obj)) { // is dict of value
510
  $params = $obj;
511
+ $params['validity'] = $validity;
512
+ $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour;
513
+ $params['maxHitsPerQuery'] = $maxHitsPerQuery;
514
  } else {
515
+ $params = [
516
+ 'acl' => $obj,
517
+ 'validity' => $validity,
518
+ 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour,
519
+ 'maxHitsPerQuery' => $maxHitsPerQuery,
520
+ ];
521
  }
522
 
523
  if ($indexes != null) {
524
  $params['indexes'] = $indexes;
525
  }
526
+
527
+ return $this->request(
528
+ $this->context,
529
+ 'POST',
530
+ '/1/keys',
531
+ [],
532
+ $params,
533
+ $this->context->writeHostsArray,
534
+ $this->context->connectTimeout,
535
+ $this->context->readTimeout
536
+ );
537
  }
538
 
539
+ /**
540
+ * Update a user key.
541
+ *
542
+ * @param string $key
543
+ * @param mixed $obj can be two different parameters:
544
+ * The list of parameters for this key. Defined by a array that
545
+ * can contains the following values:
546
+ * - acl: array of string
547
+ * - indices: array of string
548
+ * - validity: int
549
+ * - referrers: array of string
550
+ * - description: string
551
+ * - maxHitsPerQuery: integer
552
+ * - queryParameters: string
553
+ * - maxQueriesPerIPPerHour: integer
554
+ * Or the list of ACL for this key. Defined by an array of NSString that
555
+ * can contains the following values:
556
+ * - search: allow to search (https and http)
557
+ * - addObject: allows to add/update an object in the index (https only)
558
+ * - deleteObject : allows to delete an existing object (https only)
559
+ * - deleteIndex : allows to delete index content (https only)
560
+ * - settings : allows to get index settings (https only)
561
+ * - editSettings : allows to change index settings (https only)
562
+ * @param int $validity the number of seconds after which the key will be automatically removed (0 means
563
+ * no time limit for this key)
564
+ * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
565
+ * Defaults to 0 (no rate limit).
566
+ * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults
567
+ * to 0 (unlimited)
568
+ * @param array|null $indexes Specify the list of indices to target (null means all)
569
+ *
570
+ * @return mixed
571
+ *
572
+ * @throws AlgoliaException
573
+ */
574
+ public function updateUserKey(
575
+ $key,
576
+ $obj,
577
+ $validity = 0,
578
+ $maxQueriesPerIPPerHour = 0,
579
+ $maxHitsPerQuery = 0,
580
+ $indexes = null
581
+ ) {
582
  if ($obj !== array_values($obj)) { // is dict of value
583
  $params = $obj;
584
+ $params['validity'] = $validity;
585
+ $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour;
586
+ $params['maxHitsPerQuery'] = $maxHitsPerQuery;
587
  } else {
588
+ $params = [
589
+ 'acl' => $obj,
590
+ 'validity' => $validity,
591
+ 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour,
592
+ 'maxHitsPerQuery' => $maxHitsPerQuery,
593
+ ];
594
  }
595
  if ($indexes != null) {
596
  $params['indexes'] = $indexes;
597
  }
598
+
599
+ return $this->request(
600
+ $this->context,
601
+ 'PUT',
602
+ '/1/keys/'.$key,
603
+ [],
604
+ $params,
605
+ $this->context->writeHostsArray,
606
+ $this->context->connectTimeout,
607
+ $this->context->readTimeout
608
+ );
609
  }
610
 
611
  /**
612
+ * Send a batch request targeting multiple indices.
613
+ *
614
+ * @param array $requests an associative array defining the batch request body
615
+ *
616
+ * @return mixed
617
  */
618
+ public function batch($requests)
619
+ {
620
+ return $this->request(
621
+ $this->context,
622
+ 'POST',
623
+ '/1/indexes/*/batch',
624
+ [],
625
+ ['requests' => $requests],
626
+ $this->context->writeHostsArray,
627
+ $this->context->connectTimeout,
628
+ $this->context->readTimeout
629
+ );
630
  }
631
 
632
+ /**
633
+ * Generate a secured and public API Key from a list of query parameters and an
634
+ * optional user token identifying the current user.
635
  *
636
+ * @param string $privateApiKey your private API Key
637
+ * @param mixed $query the list of query parameters applied to the query (used as security)
638
+ * @param string|null $userToken an optional token identifying the current user
639
  *
640
+ * @return string
641
  */
642
+ public static function generateSecuredApiKey($privateApiKey, $query, $userToken = null)
643
+ {
644
+ $urlEncodedQuery = '';
645
+ if (is_array($query)) {
646
+ $queryParameters = [];
647
+ if (array_keys($query) !== array_keys(array_keys($query))) {
648
+ // array of query parameters
649
+ $queryParameters = $query;
650
+ } else {
651
+ // array of tags
652
+ $tmp = [];
653
+ foreach ($query as $tag) {
654
+ if (is_array($tag)) {
655
+ array_push($tmp, '('.implode(',', $tag).')');
656
+ } else {
657
+ array_push($tmp, $tag);
658
  }
 
 
 
659
  }
660
+ $tagFilters = implode(',', $tmp);
661
+ $queryParameters['tagFilters'] = $tagFilters;
662
+ }
663
+ if ($userToken != null && strlen($userToken) > 0) {
664
+ $queryParameters['userToken'] = $userToken;
665
+ }
666
+ $urlEncodedQuery = static::buildQuery($queryParameters);
667
+ } else {
668
+ if (strpos($query, '=') === false) {
669
+ // String of tags
670
+ $queryParameters = ['tagFilters' => $query];
671
+
672
+ if ($userToken != null && strlen($userToken) > 0) {
673
+ $queryParameters['userToken'] = $userToken;
674
+ }
675
+ $urlEncodedQuery = static::buildQuery($queryParameters);
676
+ } else {
677
+ // url encoded query
678
+ $urlEncodedQuery = $query;
679
+ if ($userToken != null && strlen($userToken) > 0) {
680
+ $urlEncodedQuery = $urlEncodedQuery.'&userToken='.urlencode($userToken);
681
+ }
682
+ }
683
+ }
684
+ $content = hash_hmac('sha256', $urlEncodedQuery, $privateApiKey).$urlEncodedQuery;
685
+
686
+ return base64_encode($content);
687
+ }
688
+
689
+ /**
690
+ * @param array $args
691
+ *
692
+ * @return string
693
+ */
694
+ public static function buildQuery($args)
695
+ {
696
+ foreach ($args as $key => $value) {
697
+ if (gettype($value) == 'array') {
698
+ $args[$key] = json_encode($value);
699
  }
 
700
  }
701
+
702
+ return http_build_query($args);
703
  }
704
 
705
+ /**
706
+ * @param ClientContext $context
707
+ * @param string $method
708
+ * @param string $path
709
+ * @param array $params
710
+ * @param array $data
711
+ * @param array $hostsArray
712
+ * @param int $connectTimeout
713
+ * @param int $readTimeout
714
+ *
715
+ * @return mixed
716
+ *
717
+ * @throws AlgoliaException
718
+ */
719
+ public function request(
720
+ $context,
721
+ $method,
722
+ $path,
723
+ $params,
724
+ $data,
725
+ $hostsArray,
726
+ $connectTimeout,
727
+ $readTimeout
728
+ ) {
729
+ $exceptions = [];
730
  $cnt = 0;
731
  foreach ($hostsArray as &$host) {
732
  $cnt += 1;
736
  }
737
  try {
738
  $res = $this->doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout);
739
+ if ($res !== null) {
740
  return $res;
741
+ }
742
  } catch (AlgoliaException $e) {
743
  throw $e;
744
  } catch (\Exception $e) {
745
  $exceptions[$host] = $e->getMessage();
746
  }
747
  }
748
+ throw new AlgoliaException('Hosts unreachable: '.implode(',', $exceptions));
749
  }
750
 
751
+ /**
752
+ * @param ClientContext $context
753
+ * @param string $method
754
+ * @param string $host
755
+ * @param string $path
756
+ * @param array $params
757
+ * @param array $data
758
+ * @param int $connectTimeout
759
+ * @param int $readTimeout
760
+ *
761
+ * @return mixed
762
+ *
763
+ * @throws AlgoliaException
764
+ * @throws \Exception
765
+ */
766
+ public function doRequest(
767
+ $context,
768
+ $method,
769
+ $host,
770
+ $path,
771
+ $params,
772
+ $data,
773
+ $connectTimeout,
774
+ $readTimeout
775
+ ) {
776
+ if (strpos($host, 'http') === 0) {
777
+ $url = $host.$path;
778
  } else {
779
+ $url = 'https://'.$host.$path;
780
  }
781
+
782
  if ($params != null && count($params) > 0) {
783
+ $params2 = [];
784
  foreach ($params as $key => $val) {
785
  if (is_array($val)) {
786
  $params2[$key] = json_encode($val);
788
  $params2[$key] = $val;
789
  }
790
  }
791
+ $url .= '?'.http_build_query($params2);
 
792
  }
793
+
794
  // initialize curl library
795
  $curlHandle = curl_init();
796
+
797
+ // set curl options
798
+ try {
799
+ foreach ($this->curlOptions as $curlOption => $optionValue) {
800
+ curl_setopt($curlHandle, constant($curlOption), $optionValue);
801
+ }
802
+ } catch (\Exception $e) {
803
+ $this->invalidOptions($this->curlOptions, $e->getMessage());
804
+ }
805
+
806
  //curl_setopt($curlHandle, CURLOPT_VERBOSE, true);
807
  if ($context->adminAPIKey == null) {
808
+ curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([
809
+ 'X-Algolia-Application-Id: '.$context->applicationID,
810
+ 'X-Algolia-API-Key: '.$context->apiKey,
811
+ 'Content-type: application/json',
812
+ ], $context->headers));
813
  } else {
814
+ curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([
815
+ 'X-Algolia-Application-Id: '.$context->applicationID,
816
+ 'X-Algolia-API-Key: '.$context->adminAPIKey,
817
+ 'X-Forwarded-For: '.$context->endUserIP,
818
+ 'X-Algolia-UserToken: '.$context->algoliaUserToken,
819
+ 'X-Forwarded-API-Key: '.$context->rateLimitAPIKey,
820
+ 'Content-type: application/json',
821
+ ], $context->headers));
822
+ }
823
+
824
+ curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Algolia for PHP '.Version::get());
825
  //Return the output instead of printing it
826
  curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
827
  curl_setopt($curlHandle, CURLOPT_FAILONERROR, true);
828
  curl_setopt($curlHandle, CURLOPT_ENCODING, '');
829
  curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true);
830
  curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
831
+ curl_setopt($curlHandle, CURLOPT_CAINFO, $this->caInfoPath);
832
 
833
  curl_setopt($curlHandle, CURLOPT_URL, $url);
834
  $version = curl_version();
840
  curl_setopt($curlHandle, CURLOPT_TIMEOUT, $readTimeout);
841
  }
842
 
843
+ // The problem is that on (Li|U)nix, when libcurl uses the standard name resolver,
844
+ // a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm.
845
+ curl_setopt($curlHandle, CURLOPT_NOSIGNAL, 1);
846
  curl_setopt($curlHandle, CURLOPT_FAILONERROR, false);
847
 
848
  if ($method === 'GET') {
849
  curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'GET');
850
  curl_setopt($curlHandle, CURLOPT_HTTPGET, true);
851
  curl_setopt($curlHandle, CURLOPT_POST, false);
852
+ } else {
853
+ if ($method === 'POST') {
854
+ $body = ($data) ? json_encode($data) : '';
855
+ curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
856
+ curl_setopt($curlHandle, CURLOPT_POST, true);
857
+ curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
858
+ } elseif ($method === 'DELETE') {
859
+ curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE');
860
+ curl_setopt($curlHandle, CURLOPT_POST, false);
861
+ } elseif ($method === 'PUT') {
862
+ $body = ($data) ? json_encode($data) : '';
863
+ curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT');
864
+ curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
865
+ curl_setopt($curlHandle, CURLOPT_POST, true);
866
+ }
867
  }
868
  $mhandle = $context->getMHandle($curlHandle);
869
 
872
  do {
873
  $mrc = curl_multi_exec($mhandle, $running);
874
  } while ($mrc == CURLM_CALL_MULTI_PERFORM);
875
+
876
  while ($running && $mrc == CURLM_OK) {
877
  if (curl_multi_select($mhandle, 0.1) == -1) {
878
  usleep(100);
881
  $mrc = curl_multi_exec($mhandle, $running);
882
  } while ($mrc == CURLM_CALL_MULTI_PERFORM);
883
  }
884
+
885
+ $http_status = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
886
  $response = curl_multi_getcontent($curlHandle);
887
  $error = curl_error($curlHandle);
888
+
889
  if (!empty($error)) {
890
  throw new \Exception($error);
891
  }
892
+
893
  if ($http_status === 0 || $http_status === 503) {
894
  // Could not reach host or service unavailable, try with another one if we have it
895
  $context->releaseMHandle($curlHandle);
896
  curl_close($curlHandle);
897
+
898
+ return;
899
  }
900
 
901
  $answer = json_decode($response, true);
903
  curl_close($curlHandle);
904
 
905
  if (intval($http_status / 100) == 4) {
906
+ throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status + ' error');
907
+ } elseif (intval($http_status / 100) != 2) {
908
+ throw new \Exception($http_status.': '.$response);
 
909
  }
910
 
911
  switch (json_last_error()) {
921
  case JSON_ERROR_STATE_MISMATCH:
922
  $errorMsg = 'JSON parsing error: underflow or the modes mismatch';
923
  break;
924
+ // PHP 5.3 less than 1.2.2 (Ubuntu 10.04 LTS)
925
+ case defined('JSON_ERROR_UTF8') ? JSON_ERROR_UTF8 : -1:
926
  $errorMsg = 'JSON parsing error: malformed UTF-8 characters, possibly incorrectly encoded';
927
  break;
928
  case JSON_ERROR_NONE:
930
  $errorMsg = null;
931
  break;
932
  }
933
+ if ($errorMsg !== null) {
934
  throw new AlgoliaException($errorMsg);
935
+ }
936
 
937
  return $answer;
938
  }
 
939
 
940
+ /**
941
+ * Checks if curl option passed are valid curl options.
942
+ *
943
+ * @param array $curlOptions must be array but no type required while first test throw clear Exception
944
+ *
945
+ * @return array
946
+ */
947
+ protected function checkCurlOptions($curlOptions)
948
+ {
949
+ if (!is_array($curlOptions)) {
950
+ throw new \InvalidArgumentException(
951
+ sprintf(
952
+ 'AlgoliaSearch requires %s option to be array of valid curl options.',
953
+ static::CURLOPT
954
+ )
955
+ );
956
+ }
957
+
958
+ $checkedCurlOptions = array_intersect(array_keys($curlOptions), array_keys($this->getCurlConstants()));
959
+
960
+ if (count($checkedCurlOptions) !== count($curlOptions)) {
961
+ $this->invalidOptions($curlOptions);
962
+ }
963
+
964
+ return $curlOptions;
965
+ }
966
+
967
+ /**
968
+ * Get all php curl available options.
969
+ *
970
+ * @return array
971
+ */
972
+ protected function getCurlConstants()
973
+ {
974
+ if (!is_null($this->curlConstants)) {
975
+ return $this->curlConstants;
976
+ }
977
+
978
+ $curlAllConstants = get_defined_constants(true);
979
+
980
+ if (isset($curlAllConstants['curl'])) {
981
+ $curlAllConstants = $curlAllConstants['curl'];
982
+ } elseif (isset($curlAllConstants['Core'])) { // hhvm
983
+ $curlAllConstants = $curlAllConstants['Core'];
984
+ } else {
985
+ return $this->curlConstants;
986
+ }
987
+
988
+ $curlConstants = [];
989
+ foreach ($curlAllConstants as $constantName => $constantValue) {
990
+ if (strpos($constantName, 'CURLOPT') === 0) {
991
+ $curlConstants[$constantName] = $constantValue;
992
+ }
993
+ }
994
+
995
+ $this->curlConstants = $curlConstants;
996
+
997
+ return $this->curlConstants;
998
+ }
999
+
1000
+ /**
1001
+ * throw clear Exception when bad curl option is set.
1002
+ *
1003
+ * @param array $curlOptions
1004
+ * @param string $errorMsg add specific message for disambiguation
1005
+ */
1006
+ protected function invalidOptions(array $curlOptions = [], $errorMsg = '')
1007
+ {
1008
+ throw new \OutOfBoundsException(
1009
+ sprintf(
1010
+ 'AlgoliaSearch %s options keys are invalid. %s given. error message : %s',
1011
+ static::CURLOPT,
1012
+ json_encode($curlOptions),
1013
+ $errorMsg
1014
+ )
1015
+ );
1016
+ }
1017
+
1018
+ /**
1019
+ * @return PlacesIndex
1020
+ */
1021
+ private function getPlacesIndex()
1022
+ {
1023
+ return new PlacesIndex($this->context, $this);
1024
+ }
1025
+
1026
+ /**
1027
+ * @param string $appId
1028
+ * @param string $apiKey
1029
+ * @param array $hostsArray
1030
+ * @param array $options
1031
+ *
1032
+ * @return PlacesIndex
1033
+ */
1034
+ public static function initPlaces($appId, $apiKey, $hostsArray = null, $options = [])
1035
+ {
1036
+ $options['placesEnabled'] = true;
1037
+ $client = new static($appId, $apiKey, $hostsArray, $options);
1038
+
1039
+ return $client->getPlacesIndex();
1040
+ }
1041
+ }
lib/AlgoliaSearch/ClientContext.php CHANGED
@@ -1,4 +1,5 @@
1
  <?php
 
2
  /*
3
  * Copyright (c) 2013 Algolia
4
  * http://www.algolia.com/
@@ -23,53 +24,159 @@
23
  *
24
  *
25
  */
 
26
  namespace AlgoliaSearch;
27
 
28
  use Exception;
29
 
30
- class ClientContext {
31
-
 
 
 
32
  public $applicationID;
 
 
 
 
33
  public $apiKey;
 
 
 
 
34
  public $readHostsArray;
 
 
 
 
35
  public $writeHostsArray;
 
 
 
 
36
  public $curlMHandle;
 
 
 
 
37
  public $adminAPIKey;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  public $connectTimeout;
39
 
40
- function __construct($applicationID, $apiKey, $hostsArray) {
41
- $this->connectTimeout = 2; // connect timeout of 2s by default
42
- $this->readTimeout = 30; // global timeout of 30s by default
43
- $this->searchTimeout = 5; // search timeout of 5s by default
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  $this->applicationID = $applicationID;
45
  $this->apiKey = $apiKey;
46
  $this->readHostsArray = $hostsArray;
47
  $this->writeHostsArray = $hostsArray;
 
48
  if ($this->readHostsArray == null || count($this->readHostsArray) == 0) {
49
- $this->readHostsArray = array($applicationID . "-dsn.algolia.net", $applicationID . "-1.algolianet.com", $applicationID . "-2.algolianet.com", $applicationID . "-3.algolianet.com");
50
- $this->writeHostsArray = array($applicationID . ".algolia.net", $applicationID . "-1.algolianet.com", $applicationID . "-2.algolianet.com", $applicationID . "-3.algolianet.com");
51
  }
 
52
  if ($this->applicationID == null || mb_strlen($this->applicationID) == 0) {
53
  throw new Exception('AlgoliaSearch requires an applicationID.');
54
  }
 
55
  if ($this->apiKey == null || mb_strlen($this->apiKey) == 0) {
56
  throw new Exception('AlgoliaSearch requires an apiKey.');
57
  }
58
 
59
- $this->curlMHandle = NULL;
60
- $this->adminAPIKey = NULL;
61
- $this->endUserIP = NULL;
62
- $this->rateLimitAPIKey = NULL;
63
- $this->headers = array();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
- function __destruct() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  if ($this->curlMHandle != null) {
68
  curl_multi_close($this->curlMHandle);
69
  }
70
  }
71
 
72
- public function getMHandle($curlHandle) {
 
 
 
 
 
 
73
  if ($this->curlMHandle == null) {
74
  $this->curlMHandle = curl_multi_init();
75
  }
@@ -78,24 +185,58 @@ class ClientContext {
78
  return $this->curlMHandle;
79
  }
80
 
81
- public function releaseMHandle($curlHandle) {
 
 
 
 
82
  curl_multi_remove_handle($this->curlMHandle, $curlHandle);
83
  }
84
 
85
- public function setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  $this->adminAPIKey = $adminAPIKey;
87
  $this->endUserIP = $endUserIP;
88
  $this->rateLimitAPIKey = $rateLimitAPIKey;
89
  }
90
 
91
- public function disableRateLimit() {
92
- $this->adminAPIKey = NULL;
93
- $this->endUserIP = NULL;
94
- $this->rateLimitAPIKey = NULL;
95
-
 
 
 
96
  }
97
 
98
- public function setExtraHeader($key, $value) {
 
 
 
 
 
99
  $this->headers[$key] = $value;
100
  }
101
  }
1
  <?php
2
+
3
  /*
4
  * Copyright (c) 2013 Algolia
5
  * http://www.algolia.com/
24
  *
25
  *
26
  */
27
+
28
  namespace AlgoliaSearch;
29
 
30
  use Exception;
31
 
32
+ class ClientContext
33
+ {
34
+ /**
35
+ * @var string
36
+ */
37
  public $applicationID;
38
+
39
+ /**
40
+ * @var string
41
+ */
42
  public $apiKey;
43
+
44
+ /**
45
+ * @var array
46
+ */
47
  public $readHostsArray;
48
+
49
+ /**
50
+ * @var array
51
+ */
52
  public $writeHostsArray;
53
+
54
+ /**
55
+ * @var resource
56
+ */
57
  public $curlMHandle;
58
+
59
+ /**
60
+ * @var string
61
+ */
62
  public $adminAPIKey;
63
+
64
+ /**
65
+ * @var string
66
+ */
67
+ public $endUserIP;
68
+
69
+ /**
70
+ * @var string
71
+ */
72
+ public $algoliaUserToken;
73
+
74
+ /**
75
+ * @var int
76
+ */
77
  public $connectTimeout;
78
 
79
+ /**
80
+ * ClientContext constructor.
81
+ *
82
+ * @param string $applicationID
83
+ * @param string $apiKey
84
+ * @param array $hostsArray
85
+ * @param bool $placesEnabled
86
+ *
87
+ * @throws Exception
88
+ */
89
+ public function __construct($applicationID, $apiKey, $hostsArray, $placesEnabled = false)
90
+ {
91
+ // connect timeout of 2s by default
92
+ $this->connectTimeout = 2;
93
+
94
+ // global timeout of 30s by default
95
+ $this->readTimeout = 30;
96
+
97
+ // search timeout of 5s by default
98
+ $this->searchTimeout = 5;
99
+
100
  $this->applicationID = $applicationID;
101
  $this->apiKey = $apiKey;
102
  $this->readHostsArray = $hostsArray;
103
  $this->writeHostsArray = $hostsArray;
104
+
105
  if ($this->readHostsArray == null || count($this->readHostsArray) == 0) {
106
+ $this->readHostsArray = $this->getDefaultReadHosts($placesEnabled);
107
+ $this->writeHostsArray = $this->getDefaultWriteHosts();
108
  }
109
+
110
  if ($this->applicationID == null || mb_strlen($this->applicationID) == 0) {
111
  throw new Exception('AlgoliaSearch requires an applicationID.');
112
  }
113
+
114
  if ($this->apiKey == null || mb_strlen($this->apiKey) == 0) {
115
  throw new Exception('AlgoliaSearch requires an apiKey.');
116
  }
117
 
118
+ $this->curlMHandle = null;
119
+ $this->adminAPIKey = null;
120
+ $this->endUserIP = null;
121
+ $this->algoliaUserToken = null;
122
+ $this->rateLimitAPIKey = null;
123
+ $this->headers = [];
124
+ }
125
+
126
+ /**
127
+ * @param bool $placesEnabled
128
+ *
129
+ * @return array
130
+ */
131
+ private function getDefaultReadHosts($placesEnabled)
132
+ {
133
+ if ($placesEnabled) {
134
+ return [
135
+ 'places-dsn.algolia.net',
136
+ 'places-1.algolianet.com',
137
+ 'places-2.algolianet.com',
138
+ 'places-3.algolianet.com',
139
+ ];
140
+ }
141
+
142
+ return [
143
+ $this->applicationID.'-dsn.algolia.net',
144
+ $this->applicationID.'-1.algolianet.com',
145
+ $this->applicationID.'-2.algolianet.com',
146
+ $this->applicationID.'-3.algolianet.com',
147
+ ];
148
  }
149
 
150
+ /**
151
+ * @return array
152
+ */
153
+ private function getDefaultWriteHosts()
154
+ {
155
+ return [
156
+ $this->applicationID.'.algolia.net',
157
+ $this->applicationID.'-1.algolianet.com',
158
+ $this->applicationID.'-2.algolianet.com',
159
+ $this->applicationID.'-3.algolianet.com',
160
+ ];
161
+ }
162
+
163
+ /**
164
+ * Closes eventually opened curl handles.
165
+ */
166
+ public function __destruct()
167
+ {
168
  if ($this->curlMHandle != null) {
169
  curl_multi_close($this->curlMHandle);
170
  }
171
  }
172
 
173
+ /**
174
+ * @param $curlHandle
175
+ *
176
+ * @return resource
177
+ */
178
+ public function getMHandle($curlHandle)
179
+ {
180
  if ($this->curlMHandle == null) {
181
  $this->curlMHandle = curl_multi_init();
182
  }
185
  return $this->curlMHandle;
186
  }
187
 
188
+ /**
189
+ * @param $curlHandle
190
+ */
191
+ public function releaseMHandle($curlHandle)
192
+ {
193
  curl_multi_remove_handle($this->curlMHandle, $curlHandle);
194
  }
195
 
196
+ /**
197
+ * @param string $ip
198
+ */
199
+ public function setForwardedFor($ip)
200
+ {
201
+ $this->endUserIP = $ip;
202
+ }
203
+
204
+ /**
205
+ * @param string $token
206
+ */
207
+ public function setAlgoliaUserToken($token)
208
+ {
209
+ $this->algoliaUserToken = $token;
210
+ }
211
+
212
+ /**
213
+ * @param string $adminAPIKey
214
+ * @param string $endUserIP
215
+ * @param string $rateLimitAPIKey
216
+ */
217
+ public function setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey)
218
+ {
219
  $this->adminAPIKey = $adminAPIKey;
220
  $this->endUserIP = $endUserIP;
221
  $this->rateLimitAPIKey = $rateLimitAPIKey;
222
  }
223
 
224
+ /**
225
+ * Disables the rate limit.
226
+ */
227
+ public function disableRateLimit()
228
+ {
229
+ $this->adminAPIKey = null;
230
+ $this->endUserIP = null;
231
+ $this->rateLimitAPIKey = null;
232
  }
233
 
234
+ /**
235
+ * @param string $key
236
+ * @param string $value
237
+ */
238
+ public function setExtraHeader($key, $value)
239
+ {
240
  $this->headers[$key] = $value;
241
  }
242
  }
lib/AlgoliaSearch/Index.php CHANGED
@@ -1,4 +1,5 @@
1
  <?php
 
2
  /*
3
  * Copyright (c) 2013 Algolia
4
  * http://www.algolia.com/
@@ -23,310 +24,527 @@
23
  *
24
  *
25
  */
 
26
  namespace AlgoliaSearch;
27
 
28
  /*
29
  * Contains all the functions related to one index
30
  * You should use Client.initIndex(indexName) to retrieve this object
31
  */
32
- class Index {
 
 
 
 
 
33
 
34
- public $indexName;
 
 
35
  private $client;
 
 
 
 
 
 
 
 
 
36
  private $urlIndexName;
37
 
38
- /*
39
- * Index initialization (You should not call this initialized yourself)
 
 
 
 
 
 
40
  */
41
- public function __construct($context, $client, $indexName) {
 
42
  $this->context = $context;
43
  $this->client = $client;
44
  $this->indexName = $indexName;
45
  $this->urlIndexName = urlencode($indexName);
46
  }
47
 
48
- /*
49
- * Perform batch operation on several objects
 
 
 
 
 
 
50
  *
51
- * @param objects contains an array of objects to update (each object must contains an objectID attribute)
52
- * @param objectIDKey the key in each object that contains the objectID
53
- * @param objectActionKey the key in each object that contains the action to perform (addObject, updateObject, deleteObject or partialUpdateObject)
54
  */
55
- public function batchObjects($objects, $objectIDKey = "objectID", $objectActionKey = "objectAction") {
56
- $requests = array();
 
 
 
 
 
 
 
 
57
 
58
  foreach ($objects as $obj) {
59
  // If no or invalid action, assume updateObject
60
- if (! isset($obj[$objectActionKey]) || ! in_array($obj[$objectActionKey], array('addObject', 'updateObject', 'deleteObject', 'partialUpdateObject', 'partialUpdateObjectNoCreate'))) {
61
  throw new \Exception('invalid or no action detected');
62
  }
63
 
64
  $action = $obj[$objectActionKey];
65
- unset($obj[$objectActionKey]); // The action key is not included in the object
66
 
67
- $req = array("action" => $action, "body" => $obj);
 
 
 
68
 
69
  if (array_key_exists($objectIDKey, $obj)) {
70
- $req["objectID"] = (string) $obj[$objectIDKey];
71
  }
72
 
73
  $requests[] = $req;
74
  }
75
 
76
- return $this->batch(array("requests" => $requests));
77
  }
78
 
79
- /*
80
- * Add an object in this index
81
  *
82
- * @param content contains the object to add inside the index.
83
- * The object is represented by an associative array
84
- * @param objectID (optional) an objectID you want to attribute to this object
85
- * (if the attribute already exist the old object will be overwrite)
 
 
86
  */
87
- public function addObject($content, $objectID = null) {
88
-
89
  if ($objectID === null) {
90
- return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName, array(), $content, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
91
- } else {
92
- return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($objectID), array(), $content, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
93
  }
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
 
96
- /*
97
- * Add several objects
98
  *
99
- * @param objects contains an array of objects to add. If the object contains an objectID
 
 
 
100
  */
101
- public function addObjects($objects, $objectIDKey = "objectID") {
102
- $requests = $this->buildBatch("addObject", $objects, true, $objectIDKey);
 
 
103
  return $this->batch($requests);
104
  }
105
 
106
- /*
107
- * Get an object from this index
108
  *
109
- * @param objectID the unique identifier of the object to retrieve
110
- * @param attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string separated by ","
 
 
 
111
  */
112
- public function getObject($objectID, $attributesToRetrieve = null) {
 
113
  $id = urlencode($objectID);
114
- if ($attributesToRetrieve === null)
115
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/" . $id, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
116
- else
117
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/" . $id, array("attributes" => $attributesToRetrieve), null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
- /*
121
- * Get several objects from this index
 
 
122
  *
123
- * @param objectIDs the array of unique identifier of objects to retrieve
 
 
124
  */
125
- public function getObjects($objectIDs) {
 
126
  if ($objectIDs == null) {
127
  throw new \Exception('No list of objectID provided');
128
  }
129
- $requests = array();
 
130
  foreach ($objectIDs as $object) {
131
- $req = array("indexName" => $this->indexName, "objectID" => $object);
132
  array_push($requests, $req);
133
  }
134
- return $this->client->request($this->context, "POST", "/1/indexes/*/objects", array(), array("requests" => $requests), $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
- /*
138
- * Update partially an object (only update attributes passed in argument)
 
 
 
 
139
  *
140
- * @param partialObject contains the object attributes to override, the
141
- * object must contains an objectID attribute
 
142
  */
143
- public function partialUpdateObject($partialObject, $createIfNotExists = true) {
144
- return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($partialObject["objectID"]) . "/partial" . ($createIfNotExists ? "" : "?createIfNotExists=false"), array(), $partialObject, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
 
 
145
  }
146
 
147
- /*
148
- * Partially Override the content of several objects
 
 
 
 
149
  *
150
- * @param objects contains an array of objects to update (each object must contains a objectID attribute)
151
  */
152
- public function partialUpdateObjects($objects, $objectIDKey = "objectID", $createIfNotExists = true) {
 
153
  if ($createIfNotExists) {
154
- $requests = $this->buildBatch("partialUpdateObject", $objects, true, $objectIDKey);
155
  } else {
156
- $requests = $this->buildBatch("partialUpdateObjectNoCreate", $objects, true, $objectIDKey);
157
  }
 
158
  return $this->batch($requests);
159
  }
160
 
161
- /*
162
- * Override the content of object
 
 
163
  *
164
- * @param object contains the object to save, the object must contains an objectID attribute
165
  */
166
- public function saveObject($object) {
167
- return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($object["objectID"]), array(), $object, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
168
  }
169
 
170
- /*
171
- * Override the content of several objects
 
 
 
172
  *
173
- * @param objects contains an array of objects to update (each object must contains a objectID attribute)
174
  */
175
- public function saveObjects($objects, $objectIDKey = "objectID") {
176
- $requests = $this->buildBatch("updateObject", $objects, true, $objectIDKey);
 
 
177
  return $this->batch($requests);
178
  }
179
 
180
- /*
181
- * Delete an object from the index
 
 
 
 
182
  *
183
- * @param objectID the unique identifier of object to delete
 
184
  */
185
- public function deleteObject($objectID) {
 
186
  if ($objectID == null || mb_strlen($objectID) == 0) {
187
  throw new \Exception('objectID is mandatory');
188
  }
189
- return $this->client->request($this->context, "DELETE", "/1/indexes/" . $this->urlIndexName . "/" . urlencode($objectID), null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
190
  }
191
 
192
- /*
193
- * Delete several objects
 
 
194
  *
195
- * @param objects contains an array of objectIDs to delete. If the object contains an objectID
196
  */
197
- public function deleteObjects($objects) {
198
- $objectIDs = array();
 
199
  foreach ($objects as $key => $id) {
200
- $objectIDs[$key] = array('objectID' => $id);
201
  }
202
- $requests = $this->buildBatch("deleteObject", $objectIDs, true);
 
203
  return $this->batch($requests);
204
  }
205
 
206
- /*
207
- * Delete all objects matching a query
208
  *
209
- * @param query the query string
210
- * @param params the optional query parameters
 
 
 
 
211
  */
212
- public function deleteByQuery($query, $args = array()) {
213
- $params["attributeToRetrieve"] = array('objectID');
214
- $params["hitsPerPage"] = 1000;
 
 
 
215
  $results = $this->search($query, $args);
216
  while ($results['nbHits'] != 0) {
217
- $objectIDs = array();
218
  foreach ($results['hits'] as $elt) {
219
  array_push($objectIDs, $elt['objectID']);
220
  }
221
  $res = $this->deleteObjects($objectIDs);
 
 
 
222
  $this->waitTask($res['taskID']);
223
  $results = $this->search($query, $args);
224
  }
225
  }
226
 
227
- /*
228
- * Search inside the index
229
- *
230
- * @param query the full text query
231
- * @param args (optional) if set, contains an associative array with query parameters:
232
- * - page: (integer) Pagination parameter used to select the page to retrieve.
233
- * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
234
- * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20.
235
- * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
236
- * Attributes are separated with a comma (for example "name,address").
237
- * You can also use a string array encoding (for example ["name","address"]).
238
- * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index.
239
- * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query.
240
- * Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]).
241
- * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted.
242
- * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted.
243
- * A matchLevel is returned for each highlighted attribute and can contain:
244
- * - full: if all the query terms were found in the attribute,
245
- * - partial: if only some of the query terms were found,
246
- * - none: if none of the query terms were found.
247
- * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`).
248
- * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10).
249
- * You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed.
250
- * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3.
251
- * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7.
252
- * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute.
253
- * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
254
- * For example aroundLatLng=47.316669,5.016670).
255
- * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision
256
- * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter).
257
- * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
258
- * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
259
- * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
260
- * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
261
- * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma.
262
- * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`.
263
- * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000.
264
- * You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]).
265
- * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas.
266
- * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
267
- * You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3).
268
- * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
269
- * - facetFilters: filter the query by a list of facets.
270
- * Facets are separated by commas and each facet is encoded as `attributeName:value`.
271
- * For example: `facetFilters=category:Book,author:John%20Doe`.
272
- * You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`).
273
- * - facets: List of object attributes that you want to use for faceting.
274
- * Attributes are separated with a comma (for example `"category,author"` ).
275
- * You can also use a JSON string array encoding (for example ["category","author"]).
276
- * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter.
277
- * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**.
278
- * - queryType: select how the query words are interpreted, it can be one of the following value:
279
- * - prefixAll: all query words are interpreted as prefixes,
280
- * - prefixLast: only the last word is interpreted as a prefix (default behavior),
281
- * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
282
- * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query.
283
- * The list of words is comma separated.
284
- * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set.
285
- * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter,
286
- * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results.
287
- * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best
288
- * one is kept and others are removed.
289
- */
290
- public function search($query, $args = null) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  if ($args === null) {
292
- $args = array();
293
  }
294
- $args["query"] = $query;
295
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName, $args, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->searchTimeout);
 
 
 
 
 
 
 
 
 
 
296
  }
297
 
298
- /*
299
- * Perform a search with disjunctive facets generating as many queries as number of disjunctive facets
 
 
 
 
 
 
300
  *
301
- * @param query the query
302
- * @param disjunctive_facets the array of disjunctive facets
303
- * @param params a hash representing the regular query parameters
304
- * @param refinements a hash ("string" -> ["array", "of", "refined", "values"]) representing the current refinements
305
- * ex: { "my_facet1" => ["my_value1", ["my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] }
306
  */
307
- public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = array(), $refinements = array()) {
308
- if (gettype($disjunctive_facets) != "string" && gettype($disjunctive_facets) != "array") {
309
- throw new AlgoliaException("Argument \"disjunctive_facets\" must be a String or an Array");
 
310
  }
311
- if (gettype($refinements) != "array") {
312
- throw new AlgoliaException("Argument \"refinements\" must be a Hash of Arrays");
 
313
  }
314
 
315
- if (gettype($disjunctive_facets) == "string") {
316
- $disjunctive_facets = split(",", $disjunctive_facets);
317
  }
318
 
319
- $disjunctive_refinements = array();
320
  foreach ($refinements as $key => $value) {
321
  if (in_array($key, $disjunctive_facets)) {
322
  $disjunctive_refinements[$key] = $value;
323
  }
324
  }
325
- $queries = array();
326
- $filters = array();
327
 
328
  foreach ($refinements as $key => $value) {
329
- $r = array_map(function ($val) use ($key) { return $key . ":" . $val;}, $value);
 
 
 
 
 
330
 
331
  if (in_array($key, $disjunctive_refinements)) {
332
  $filter = array_merge($filters, $r);
@@ -334,15 +552,20 @@ class Index {
334
  array_push($filters, $r);
335
  }
336
  }
337
- $params["indexName"] = $this->indexName;
338
- $params["query"] = $query;
339
- $params["facetFilters"] = $filters;
340
  array_push($queries, $params);
341
  foreach ($disjunctive_facets as $disjunctive_facet) {
342
- $filters = array();
343
  foreach ($refinements as $key => $value) {
344
  if ($key != $disjunctive_facet) {
345
- $r = array_map(function($val) use($key) { return $key . ":" . $val;}, $value);
 
 
 
 
 
346
 
347
  if (in_array($key, $disjunctive_refinements)) {
348
  $filter = array_merge($filters, $r);
@@ -351,22 +574,22 @@ class Index {
351
  }
352
  }
353
  }
354
- $params["indexName"] = $this->indexName;
355
- $params["query"] = $query;
356
- $params["facetFilters"] = $filters;
357
- $params["page"] = 0;
358
- $params["hitsPerPage"] = 0;
359
- $params["attributesToRetrieve"] = array();
360
- $params["attributesToHighlight"] = array();
361
- $params["attributesToSnippet"] = array();
362
- $params["facets"] = $disjunctive_facet;
363
- $params["analytics"] = false;
364
  array_push($queries, $params);
365
  }
366
  $answers = $this->client->multipleQueries($queries);
367
 
368
  $aggregated_answer = $answers['results'][0];
369
- $aggregated_answer['disjunctiveFacets'] = array();
370
  for ($i = 1; $i < count($answers['results']); $i++) {
371
  foreach ($answers['results'][$i]['facets'] as $key => $facet) {
372
  $aggregated_answer['disjunctiveFacets'][$key] = $facet;
@@ -380,245 +603,499 @@ class Index {
380
  }
381
  }
382
  }
 
383
  return $aggregated_answer;
384
  }
385
 
386
- /*
387
- * Browse all index content
 
 
 
 
 
 
388
  *
389
- * @param page Pagination parameter used to select the page to retrieve.
390
- * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
391
- * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000.
392
  */
393
- public function browse($page = 0, $hitsPerPage = 1000) {
394
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/browse",
395
- array("page" => $page, "hitsPerPage" => $hitsPerPage), null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
396
  }
397
 
398
- /*
399
  * Wait the publication of a task on the server.
400
  * All server task are asynchronous and you can check with this method that the task is published.
401
  *
402
- * @param taskID the id of the task returned by server
403
- * @param timeBeforeRetry the time in milliseconds before retry (default = 100ms)
 
 
404
  */
405
- public function waitTask($taskID, $timeBeforeRetry = 100) {
 
406
  while (true) {
407
- $res = $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/task/" . $taskID, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
408
- if ($res["status"] === "published")
409
  return $res;
 
410
  usleep($timeBeforeRetry * 1000);
411
  }
412
  }
413
 
414
- /*
415
- * Get settings of this index
 
 
 
416
  *
 
417
  */
418
- public function getSettings() {
419
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/settings", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
420
  }
421
 
422
- /*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  * This function deletes the index content. Settings and index specific API keys are kept untouched.
 
 
 
 
424
  */
425
- public function clearIndex() {
426
- return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/clear", null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
427
  }
428
 
429
- /*
430
- * Set settings for this index
431
- *
432
- * @param settigns the settings object that can contains :
433
- * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3).
434
- * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7).
435
- * - hitsPerPage: (integer) the number of hits per page (default = 10).
436
- * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
437
- * If set to null, all attributes are retrieved.
438
- * - attributesToHighlight: (array of strings) default list of attributes to highlight.
439
- * If set to null, all indexed attributes are highlighted.
440
- * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords).
441
- * By default no snippet is computed. If set to null, no snippet is computed.
442
- * - attributesToIndex: (array of strings) the list of fields you want to index.
443
- * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
444
- * This parameter has two important uses:
445
- * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to
446
- * retrieve it but you don't want to search in the base64 string.
447
- * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of
448
- * the list will be considered more important than matches in attributes further down the list.
449
- * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable
450
- * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"].
451
- * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
452
- * 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.
453
- * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled
454
- * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results.
455
- * 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.
456
- * - ranking: (array of strings) controls the way results are sorted.
457
- * We have six available criteria:
458
- * - typo: sort according to number of typos,
459
- * - geo: sort according to decreassing distance when performing a geo-location based search,
460
- * - proximity: sort according to the proximity of query words in hits,
461
- * - attribute: sort according to the order of attributes defined by attributesToIndex,
462
- * - exact:
463
- * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others.
464
- * 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
465
- * show starting by the v letter before it.
466
- * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix).
467
- * - custom: sort according to a user defined formula set in **customRanking** attribute.
468
- * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
469
- * - customRanking: (array of strings) lets you specify part of the ranking.
470
- * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator.
471
- * For example `"customRanking" => ["desc(population)", "asc(name)"]`
472
- * - queryType: Select how the query words are interpreted, it can be one of the following value:
473
- * - prefixAll: all query words are interpreted as prefixes,
474
- * - prefixLast: only the last word is interpreted as a prefix (default behavior),
475
- * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
476
- * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>").
477
- * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>").
478
- * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query.
479
- */
480
- public function setSettings($settings) {
481
- return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/settings", array(), $settings, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  }
483
 
484
- /*
485
- * List all existing user keys associated to this index with their associated ACLs
486
  *
 
 
 
487
  */
488
- public function listUserKeys() {
489
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/keys", null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
490
  }
491
 
492
- /*
493
- * Get ACL of a user key associated to this index
494
  *
 
 
 
 
 
495
  */
496
- public function getUserKeyACL($key) {
497
- return $this->client->request($this->context, "GET", "/1/indexes/" . $this->urlIndexName . "/keys/" . $key, null, null, $this->context->readHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
498
  }
499
 
500
- /*
501
- * Delete an existing user key associated to this index
 
 
502
  *
 
 
 
503
  */
504
- public function deleteUserKey($key) {
505
- return $this->client->request($this->context, "DELETE", "/1/indexes/" . $this->urlIndexName . "/keys/" . $key, null, null, $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
 
506
  }
507
 
508
- /*
509
- * Create a new user key associated to this index
510
- *
511
- * @param obj can be two different parameters:
512
- * The list of parameters for this key. Defined by a NSDictionary that
513
- * can contains the following values:
514
- * - acl: array of string
515
- * - indices: array of string
516
- * - validity: int
517
- * - referers: array of string
518
- * - description: string
519
- * - maxHitsPerQuery: integer
520
- * - queryParameters: string
521
- * - maxQueriesPerIPPerHour: integer
522
- * Or the list of ACL for this key. Defined by an array of NSString that
523
- * can contains the following values:
524
- * - search: allow to search (https and http)
525
- * - addObject: allows to add/update an object in the index (https only)
526
- * - deleteObject : allows to delete an existing object (https only)
527
- * - deleteIndex : allows to delete index content (https only)
528
- * - settings : allows to get index settings (https only)
529
- * - editSettings : allows to change index settings (https only)
530
- * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
531
- * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit).
532
- * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited)
533
- */
534
- public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) {
535
- if ($obj !== array_values($obj)) { // is dict of value
 
 
 
 
 
 
 
 
 
536
  $params = $obj;
537
- $params["validity"] = $validity;
538
- $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour;
539
- $params["maxHitsPerQuery"] = $maxHitsPerQuery;
540
  } else {
541
- $params = array(
542
- "acl" => $obj,
543
- "validity" => $validity,
544
- "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour,
545
- "maxHitsPerQuery" => $maxHitsPerQuery
546
- );
547
  }
548
- return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/keys", array(), $params,
549
- $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
550
  }
551
 
552
- /*
553
- * Update a user key associated to this index
554
- *
555
- * @param obj can be two different parameters:
556
- * The list of parameters for this key. Defined by a NSDictionary that
557
- * can contains the following values:
558
- * - acl: array of string
559
- * - indices: array of string
560
- * - validity: int
561
- * - referers: array of string
562
- * - description: string
563
- * - maxHitsPerQuery: integer
564
- * - queryParameters: string
565
- * - maxQueriesPerIPPerHour: integer
566
- * Or the list of ACL for this key. Defined by an array of NSString that
567
- * can contains the following values:
568
- * - search: allow to search (https and http)
569
- * - addObject: allows to add/update an object in the index (https only)
570
- * - deleteObject : allows to delete an existing object (https only)
571
- * - deleteIndex : allows to delete index content (https only)
572
- * - settings : allows to get index settings (https only)
573
- * - editSettings : allows to change index settings (https only)
574
- * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
575
- * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. Defaults to 0 (no rate limit).
576
- * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults to 0 (unlimited)
577
- */
578
- public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) {
579
- if ($obj !== array_values($obj)) { // is dict of value
 
 
 
 
 
 
 
 
 
 
580
  $params = $obj;
581
- $params["validity"] = $validity;
582
- $params["maxQueriesPerIPPerHour"] = $maxQueriesPerIPPerHour;
583
- $params["maxHitsPerQuery"] = $maxHitsPerQuery;
584
  } else {
585
- $params = array(
586
- "acl" => $obj,
587
- "validity" => $validity,
588
- "maxQueriesPerIPPerHour" => $maxQueriesPerIPPerHour,
589
- "maxHitsPerQuery" => $maxHitsPerQuery
590
- );
591
  }
592
- return $this->client->request($this->context, "PUT", "/1/indexes/" . $this->urlIndexName . "/keys/" . $key , array(), $params,
593
- $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
594
  }
595
 
596
  /**
597
- * Send a batch request
598
- * @param $requests an associative array defining the batch request body
 
 
 
599
  */
600
- public function batch($requests) {
601
- return $this->client->request($this->context, "POST", "/1/indexes/" . $this->urlIndexName . "/batch", array(), $requests,
602
- $this->context->writeHostsArray, $this->context->connectTimeout, $this->context->readTimeout);
 
 
 
 
 
 
 
 
 
603
  }
604
 
605
  /**
606
- * Build a batch request
607
- * @param $action the batch action
608
- * @param $objects the array of objects
609
- * @param $withObjectID set an 'objectID' attribute
610
- * @param $objectIDKey the objectIDKey
 
 
611
  * @return array
612
  */
613
- private function buildBatch($action, $objects, $withObjectID, $objectIDKey = "objectID") {
614
- $requests = array();
 
615
  foreach ($objects as $obj) {
616
- $req = array("action" => $action, "body" => $obj);
617
  if ($withObjectID && array_key_exists($objectIDKey, $obj)) {
618
- $req["objectID"] = (string) $obj[$objectIDKey];
619
  }
620
  array_push($requests, $req);
621
  }
622
- return array("requests" => $requests);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  }
624
  }
1
  <?php
2
+
3
  /*
4
  * Copyright (c) 2013 Algolia
5
  * http://www.algolia.com/
24
  *
25
  *
26
  */
27
+
28
  namespace AlgoliaSearch;
29
 
30
  /*
31
  * Contains all the functions related to one index
32
  * You should use Client.initIndex(indexName) to retrieve this object
33
  */
34
+ class Index
35
+ {
36
+ /**
37
+ * @var ClientContext
38
+ */
39
+ private $context;
40
 
41
+ /**
42
+ * @var Client
43
+ */
44
  private $client;
45
+
46
+ /**
47
+ * @var string
48
+ */
49
+ public $indexName;
50
+
51
+ /**
52
+ * @var string
53
+ */
54
  private $urlIndexName;
55
 
56
+ /**
57
+ * Index initialization (You should not instantiate this yourself).
58
+ *
59
+ * @param ClientContext $context
60
+ * @param Client $client
61
+ * @param string $indexName
62
+ *
63
+ * @internal
64
  */
65
+ public function __construct(ClientContext $context, Client $client, $indexName)
66
+ {
67
  $this->context = $context;
68
  $this->client = $client;
69
  $this->indexName = $indexName;
70
  $this->urlIndexName = urlencode($indexName);
71
  }
72
 
73
+ /**
74
+ * Perform batch operation on several objects.
75
+ *
76
+ * @param array $objects contains an array of objects to update (each object must contains an objectID
77
+ * attribute)
78
+ * @param string $objectIDKey the key in each object that contains the objectID
79
+ * @param string $objectActionKey the key in each object that contains the action to perform (addObject, updateObject,
80
+ * deleteObject or partialUpdateObject)
81
  *
82
+ * @return mixed
83
+ *
84
+ * @throws \Exception
85
  */
86
+ public function batchObjects($objects, $objectIDKey = 'objectID', $objectActionKey = 'objectAction')
87
+ {
88
+ $requests = [];
89
+ $allowedActions = [
90
+ 'addObject',
91
+ 'updateObject',
92
+ 'deleteObject',
93
+ 'partialUpdateObject',
94
+ 'partialUpdateObjectNoCreate',
95
+ ];
96
 
97
  foreach ($objects as $obj) {
98
  // If no or invalid action, assume updateObject
99
+ if (!isset($obj[$objectActionKey]) || !in_array($obj[$objectActionKey], $allowedActions)) {
100
  throw new \Exception('invalid or no action detected');
101
  }
102
 
103
  $action = $obj[$objectActionKey];
 
104
 
105
+ // The action key is not included in the object
106
+ unset($obj[$objectActionKey]);
107
+
108
+ $req = ['action' => $action, 'body' => $obj];
109
 
110
  if (array_key_exists($objectIDKey, $obj)) {
111
+ $req['objectID'] = (string) $obj[$objectIDKey];
112
  }
113
 
114
  $requests[] = $req;
115
  }
116
 
117
+ return $this->batch(['requests' => $requests]);
118
  }
119
 
120
+ /**
121
+ * Add an object in this index.
122
  *
123
+ * @param array $content contains the object to add inside the index.
124
+ * The object is represented by an associative array
125
+ * @param string|null $objectID (optional) an objectID you want to attribute to this object
126
+ * (if the attribute already exist the old object will be overwrite)
127
+ *
128
+ * @return mixed
129
  */
130
+ public function addObject($content, $objectID = null)
131
+ {
132
  if ($objectID === null) {
133
+ return $this->client->request(
134
+ $this->context,
135
+ 'POST',
136
+ '/1/indexes/'.$this->urlIndexName,
137
+ [],
138
+ $content,
139
+ $this->context->writeHostsArray,
140
+ $this->context->connectTimeout,
141
+ $this->context->readTimeout
142
+ );
143
  }
144
+
145
+ return $this->client->request(
146
+ $this->context,
147
+ 'PUT',
148
+ '/1/indexes/'.$this->urlIndexName.'/'.urlencode($objectID),
149
+ [],
150
+ $content,
151
+ $this->context->writeHostsArray,
152
+ $this->context->connectTimeout,
153
+ $this->context->readTimeout
154
+ );
155
  }
156
 
157
+ /**
158
+ * Add several objects.
159
  *
160
+ * @param array $objects contains an array of objects to add. If the object contains an objectID
161
+ * @param string $objectIDKey
162
+ *
163
+ * @return mixed
164
  */
165
+ public function addObjects($objects, $objectIDKey = 'objectID')
166
+ {
167
+ $requests = $this->buildBatch('addObject', $objects, true, $objectIDKey);
168
+
169
  return $this->batch($requests);
170
  }
171
 
172
+ /**
173
+ * Get an object from this index.
174
  *
175
+ * @param $objectID the unique identifier of the object to retrieve
176
+ * @param $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string
177
+ * separated by ","
178
+ *
179
+ * @return mixed
180
  */
181
+ public function getObject($objectID, $attributesToRetrieve = null)
182
+ {
183
  $id = urlencode($objectID);
184
+ if ($attributesToRetrieve === null) {
185
+ return $this->client->request(
186
+ $this->context,
187
+ 'GET',
188
+ '/1/indexes/'.$this->urlIndexName.'/'.$id,
189
+ null,
190
+ null,
191
+ $this->context->readHostsArray,
192
+ $this->context->connectTimeout,
193
+ $this->context->readTimeout
194
+ );
195
+ }
196
+
197
+ return $this->client->request(
198
+ $this->context,
199
+ 'GET',
200
+ '/1/indexes/'.$this->urlIndexName.'/'.$id,
201
+ ['attributes' => $attributesToRetrieve],
202
+ null,
203
+ $this->context->readHostsArray,
204
+ $this->context->connectTimeout,
205
+ $this->context->readTimeout
206
+ );
207
  }
208
 
209
+ /**
210
+ * Get several objects from this index.
211
+ *
212
+ * @param array $objectIDs the array of unique identifier of objects to retrieve
213
  *
214
+ * @return mixed
215
+ *
216
+ * @throws \Exception
217
  */
218
+ public function getObjects($objectIDs)
219
+ {
220
  if ($objectIDs == null) {
221
  throw new \Exception('No list of objectID provided');
222
  }
223
+
224
+ $requests = [];
225
  foreach ($objectIDs as $object) {
226
+ $req = ['indexName' => $this->indexName, 'objectID' => $object];
227
  array_push($requests, $req);
228
  }
229
+
230
+ return $this->client->request(
231
+ $this->context,
232
+ 'POST',
233
+ '/1/indexes/*/objects',
234
+ [],
235
+ ['requests' => $requests],
236
+ $this->context->readHostsArray,
237
+ $this->context->connectTimeout,
238
+ $this->context->readTimeout
239
+ );
240
  }
241
 
242
+ /**
243
+ * Update partially an object (only update attributes passed in argument).
244
+ *
245
+ * @param array $partialObject contains the object attributes to override, the
246
+ * object must contains an objectID attribute
247
+ * @param bool $createIfNotExists
248
  *
249
+ * @return mixed
250
+ *
251
+ * @throws AlgoliaException
252
  */
253
+ public function partialUpdateObject($partialObject, $createIfNotExists = true)
254
+ {
255
+ $queryString = $createIfNotExists ? '' : '?createIfNotExists=false';
256
+
257
+ return $this->client->request(
258
+ $this->context,
259
+ 'POST',
260
+ '/1/indexes/'.$this->urlIndexName.'/'.urlencode($partialObject['objectID']).'/partial'.$queryString,
261
+ [],
262
+ $partialObject,
263
+ $this->context->writeHostsArray,
264
+ $this->context->connectTimeout,
265
+ $this->context->readTimeout
266
+ );
267
  }
268
 
269
+ /**
270
+ * Partially Override the content of several objects.
271
+ *
272
+ * @param array $objects contains an array of objects to update (each object must contains a objectID attribute)
273
+ * @param string $objectIDKey
274
+ * @param bool $createIfNotExists
275
  *
276
+ * @return mixed
277
  */
278
+ public function partialUpdateObjects($objects, $objectIDKey = 'objectID', $createIfNotExists = true)
279
+ {
280
  if ($createIfNotExists) {
281
+ $requests = $this->buildBatch('partialUpdateObject', $objects, true, $objectIDKey);
282
  } else {
283
+ $requests = $this->buildBatch('partialUpdateObjectNoCreate', $objects, true, $objectIDKey);
284
  }
285
+
286
  return $this->batch($requests);
287
  }
288
 
289
+ /**
290
+ * Override the content of object.
291
+ *
292
+ * @param array $object contains the object to save, the object must contains an objectID attribute
293
  *
294
+ * @return mixed
295
  */
296
+ public function saveObject($object)
297
+ {
298
+ return $this->client->request(
299
+ $this->context,
300
+ 'PUT',
301
+ '/1/indexes/'.$this->urlIndexName.'/'.urlencode($object['objectID']),
302
+ [],
303
+ $object,
304
+ $this->context->writeHostsArray,
305
+ $this->context->connectTimeout,
306
+ $this->context->readTimeout
307
+ );
308
  }
309
 
310
+ /**
311
+ * Override the content of several objects.
312
+ *
313
+ * @param array $objects contains an array of objects to update (each object must contains a objectID attribute)
314
+ * @param string $objectIDKey
315
  *
316
+ * @return mixed
317
  */
318
+ public function saveObjects($objects, $objectIDKey = 'objectID')
319
+ {
320
+ $requests = $this->buildBatch('updateObject', $objects, true, $objectIDKey);
321
+
322
  return $this->batch($requests);
323
  }
324
 
325
+ /**
326
+ * Delete an object from the index.
327
+ *
328
+ * @param $objectID the unique identifier of object to delete
329
+ *
330
+ * @return mixed
331
  *
332
+ * @throws AlgoliaException
333
+ * @throws \Exception
334
  */
335
+ public function deleteObject($objectID)
336
+ {
337
  if ($objectID == null || mb_strlen($objectID) == 0) {
338
  throw new \Exception('objectID is mandatory');
339
  }
340
+
341
+ return $this->client->request(
342
+ $this->context,
343
+ 'DELETE',
344
+ '/1/indexes/'.$this->urlIndexName.'/'.urlencode($objectID),
345
+ null,
346
+ null,
347
+ $this->context->writeHostsArray,
348
+ $this->context->connectTimeout,
349
+ $this->context->readTimeout
350
+ );
351
  }
352
 
353
+ /**
354
+ * Delete several objects.
355
+ *
356
+ * @param array $objects contains an array of objectIDs to delete. If the object contains an objectID
357
  *
358
+ * @return mixed
359
  */
360
+ public function deleteObjects($objects)
361
+ {
362
+ $objectIDs = [];
363
  foreach ($objects as $key => $id) {
364
+ $objectIDs[$key] = ['objectID' => $id];
365
  }
366
+ $requests = $this->buildBatch('deleteObject', $objectIDs, true);
367
+
368
  return $this->batch($requests);
369
  }
370
 
371
+ /**
372
+ * Delete all objects matching a query.
373
  *
374
+ * @param string $query the query string
375
+ * @param array $args the optional query parameters
376
+ * @param bool $waitLastCall
377
+ * /!\ Be safe with "waitLastCall"
378
+ * In really rare cases you can have the number of hits smaller than the hitsPerPage
379
+ * param if you trigger the timeout of the search, in that case you won't remove all the records
380
  */
381
+ public function deleteByQuery($query, $args = [], $waitLastCall = true)
382
+ {
383
+ $args['attributesToRetrieve'] = 'objectID';
384
+ $args['hitsPerPage'] = 1000;
385
+ $args['distinct'] = false;
386
+
387
  $results = $this->search($query, $args);
388
  while ($results['nbHits'] != 0) {
389
+ $objectIDs = [];
390
  foreach ($results['hits'] as $elt) {
391
  array_push($objectIDs, $elt['objectID']);
392
  }
393
  $res = $this->deleteObjects($objectIDs);
394
+ if ($results['nbHits'] < $args['hitsPerPage'] && false === $waitLastCall) {
395
+ break;
396
+ }
397
  $this->waitTask($res['taskID']);
398
  $results = $this->search($query, $args);
399
  }
400
  }
401
 
402
+ /**
403
+ * Search inside the index.
404
+ *
405
+ * @param string $query the full text query
406
+ * @param mixed $args (optional) if set, contains an associative array with query parameters:
407
+ * - page: (integer) Pagination parameter used to select the page to retrieve.
408
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
409
+ * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page.
410
+ * Defaults to 20.
411
+ * - attributesToRetrieve: a string that contains the list of object attributes you want to
412
+ * retrieve (let you minimize the answer size). Attributes are separated with a comma (for
413
+ * example "name,address"). You can also use a string array encoding (for example
414
+ * ["name","address"]). By default, all attributes are retrieved. You can also use '*' to
415
+ * retrieve all values when an attributesToRetrieve setting is specified for your index.
416
+ * - attributesToHighlight: a string that contains the list of attributes you want to highlight
417
+ * according to the query. Attributes are separated by a comma. You can also use a string array
418
+ * encoding (for example ["name","address"]). If an attribute has no match for the query, the raw
419
+ * value is returned. By default all indexed text attributes are highlighted. You can use `*` if
420
+ * you want to highlight all textual attributes. Numerical attributes are not highlighted. A
421
+ * matchLevel is returned for each highlighted attribute and can contain:
422
+ * - full: if all the query terms were found in the attribute,
423
+ * - partial: if only some of the query terms were found,
424
+ * - none: if none of the query terms were found.
425
+ * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the
426
+ * number of words to return (syntax is `attributeName:nbWords`). Attributes are separated by a
427
+ * comma (Example: attributesToSnippet=name:10,content:10). You can also use a string array
428
+ * encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is
429
+ * computed.
430
+ * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in
431
+ * this word. Defaults to 3.
432
+ * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos
433
+ * in this word. Defaults to 7.
434
+ * - getRankingInfo: if set to 1, the result hits will contain ranking information in
435
+ * _rankingInfo attribute.
436
+ * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats
437
+ * separated by a comma). For example aroundLatLng=47.316669,5.016670). You can specify the
438
+ * maximum distance in meters with the aroundRadius parameter (in meters) and the precision for
439
+ * ranking with aroundPrecision
440
+ * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m
441
+ * will be considered as identical for "geo" ranking parameter). At indexing, you should specify
442
+ * geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409,
443
+ * "lng":2.348800}})
444
+ * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a
445
+ * rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). For example
446
+ * insideBoundingBox=47.3165,4.9665,47.3424,5.0201). At indexing, you should specify geoloc of an
447
+ * object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
448
+ * - numericFilters: a string that contains the list of numeric filters you want to apply
449
+ * separated by a comma. The syntax of one filter is `attributeName` followed by `operand`
450
+ * followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. You can have
451
+ * multiple conditions on one attribute like for example numericFilters=price>100,price<1000. You
452
+ * can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]).
453
+ * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by
454
+ * commas.
455
+ * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2
456
+ * OR tag3). You can also use a string array encoding, for example tagFilters:
457
+ * ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). At indexing, tags should be added in
458
+ * the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
459
+ * - facetFilters: filter the query by a list of facets.
460
+ * Facets are separated by commas and each facet is encoded as `attributeName:value`.
461
+ * For example: `facetFilters=category:Book,author:John%20Doe`.
462
+ * You can also use a string array encoding (for example
463
+ * `["category:Book","author:John%20Doe"]`).
464
+ * - facets: List of object attributes that you want to use for faceting.
465
+ * Attributes are separated with a comma (for example `"category,author"` ).
466
+ * You can also use a JSON string array encoding (for example ["category","author"]).
467
+ * Only attributes that have been added in **attributesForFaceting** index setting can be used in
468
+ * this parameter. You can also use `*` to perform faceting on all attributes specified in
469
+ * **attributesForFaceting**.
470
+ * - queryType: select how the query words are interpreted, it can be one of the following value:
471
+ * - prefixAll: all query words are interpreted as prefixes,
472
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
473
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
474
+ * - optionalWords: a string that contains the list of words that should be considered as
475
+ * optional when found in the query. The list of words is comma separated.
476
+ * - distinct: If set to 1, enable the distinct feature (disabled by default) if the
477
+ * attributeForDistinct index setting is set. This feature is similar to the SQL "distinct"
478
+ * keyword: when enabled in a query with the distinct=1 parameter, all hits containing a
479
+ * duplicate value for the attributeForDistinct attribute are removed from results. For example,
480
+ * if the chosen attribute is show_name and several hits have the same value for show_name, then
481
+ * only the best one is kept and others are removed.
482
+ *
483
+ * @return mixed
484
+ */
485
+ public function search($query, $args = null)
486
+ {
487
  if ($args === null) {
488
+ $args = [];
489
  }
490
+ $args['query'] = $query;
491
+
492
+ return $this->client->request(
493
+ $this->context,
494
+ 'POST',
495
+ '/1/indexes/'.$this->urlIndexName.'/query',
496
+ [],
497
+ ['params' => $this->client->buildQuery($args)],
498
+ $this->context->readHostsArray,
499
+ $this->context->connectTimeout,
500
+ $this->context->searchTimeout
501
+ );
502
  }
503
 
504
+ /**
505
+ * Perform a search with disjunctive facets generating as many queries as number of disjunctive facets.
506
+ *
507
+ * @param string $query the query
508
+ * @param array $disjunctive_facets the array of disjunctive facets
509
+ * @param array $params a hash representing the regular query parameters
510
+ * @param array $refinements a hash ("string" -> ["array", "of", "refined", "values"]) representing the current refinements
511
+ * ex: { "my_facet1" => ["my_value1", ["my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] }
512
  *
513
+ * @return mixed
514
+ *
515
+ * @throws AlgoliaException
516
+ * @throws \Exception
 
517
  */
518
+ public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = [], $refinements = [])
519
+ {
520
+ if (gettype($disjunctive_facets) != 'string' && gettype($disjunctive_facets) != 'array') {
521
+ throw new AlgoliaException('Argument "disjunctive_facets" must be a String or an Array');
522
  }
523
+
524
+ if (gettype($refinements) != 'array') {
525
+ throw new AlgoliaException('Argument "refinements" must be a Hash of Arrays');
526
  }
527
 
528
+ if (gettype($disjunctive_facets) == 'string') {
529
+ $disjunctive_facets = explode(',', $disjunctive_facets);
530
  }
531
 
532
+ $disjunctive_refinements = [];
533
  foreach ($refinements as $key => $value) {
534
  if (in_array($key, $disjunctive_facets)) {
535
  $disjunctive_refinements[$key] = $value;
536
  }
537
  }
538
+ $queries = [];
539
+ $filters = [];
540
 
541
  foreach ($refinements as $key => $value) {
542
+ $r = array_map(
543
+ function ($val) use ($key) {
544
+ return $key.':'.$val;
545
+ },
546
+ $value
547
+ );
548
 
549
  if (in_array($key, $disjunctive_refinements)) {
550
  $filter = array_merge($filters, $r);
552
  array_push($filters, $r);
553
  }
554
  }
555
+ $params['indexName'] = $this->indexName;
556
+ $params['query'] = $query;
557
+ $params['facetFilters'] = $filters;
558
  array_push($queries, $params);
559
  foreach ($disjunctive_facets as $disjunctive_facet) {
560
+ $filters = [];
561
  foreach ($refinements as $key => $value) {
562
  if ($key != $disjunctive_facet) {
563
+ $r = array_map(
564
+ function ($val) use ($key) {
565
+ return $key.':'.$val;
566
+ },
567
+ $value
568
+ );
569
 
570
  if (in_array($key, $disjunctive_refinements)) {
571
  $filter = array_merge($filters, $r);
574
  }
575
  }
576
  }
577
+ $params['indexName'] = $this->indexName;
578
+ $params['query'] = $query;
579
+ $params['facetFilters'] = $filters;
580
+ $params['page'] = 0;
581
+ $params['hitsPerPage'] = 0;
582
+ $params['attributesToRetrieve'] = [];
583
+ $params['attributesToHighlight'] = [];
584
+ $params['attributesToSnippet'] = [];
585
+ $params['facets'] = $disjunctive_facet;
586
+ $params['analytics'] = false;
587
  array_push($queries, $params);
588
  }
589
  $answers = $this->client->multipleQueries($queries);
590
 
591
  $aggregated_answer = $answers['results'][0];
592
+ $aggregated_answer['disjunctiveFacets'] = [];
593
  for ($i = 1; $i < count($answers['results']); $i++) {
594
  foreach ($answers['results'][$i]['facets'] as $key => $facet) {
595
  $aggregated_answer['disjunctiveFacets'][$key] = $facet;
603
  }
604
  }
605
  }
606
+
607
  return $aggregated_answer;
608
  }
609
 
610
+ /**
611
+ * Browse all index content.
612
+ *
613
+ * @param int $page Pagination parameter used to select the page to retrieve.
614
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
615
+ * @param int $hitsPerPage : Pagination parameter used to select the number of hits per page. Defaults to 1000.
616
+ *
617
+ * @return mixed
618
  *
619
+ * @throws AlgoliaException
 
 
620
  */
621
+ private function doBcBrowse($page = 0, $hitsPerPage = 1000)
622
+ {
623
+ return $this->client->request(
624
+ $this->context,
625
+ 'GET',
626
+ '/1/indexes/'.$this->urlIndexName.'/browse',
627
+ ['page' => $page, 'hitsPerPage' => $hitsPerPage],
628
+ null,
629
+ $this->context->readHostsArray,
630
+ $this->context->connectTimeout,
631
+ $this->context->readTimeout
632
+ );
633
  }
634
 
635
+ /**
636
  * Wait the publication of a task on the server.
637
  * All server task are asynchronous and you can check with this method that the task is published.
638
  *
639
+ * @param string $taskID the id of the task returned by server
640
+ * @param int $timeBeforeRetry the time in milliseconds before retry (default = 100ms)
641
+ *
642
+ * @return mixed
643
  */
644
+ public function waitTask($taskID, $timeBeforeRetry = 100)
645
+ {
646
  while (true) {
647
+ $res = $this->getTaskStatus($taskID);
648
+ if ($res['status'] === 'published') {
649
  return $res;
650
+ }
651
  usleep($timeBeforeRetry * 1000);
652
  }
653
  }
654
 
655
+ /**
656
+ * get the status of a task on the server.
657
+ * All server task are asynchronous and you can check with this method that the task is published or not.
658
+ *
659
+ * @param string $taskID the id of the task returned by server
660
  *
661
+ * @return mixed
662
  */
663
+ public function getTaskStatus($taskID)
664
+ {
665
+ return $this->client->request(
666
+ $this->context,
667
+ 'GET',
668
+ '/1/indexes/'.$this->urlIndexName.'/task/'.$taskID,
669
+ null,
670
+ null,
671
+ $this->context->readHostsArray,
672
+ $this->context->connectTimeout,
673
+ $this->context->readTimeout
674
+ );
675
  }
676
 
677
+ /**
678
+ * Get settings of this index.
679
+ *
680
+ * @return mixed
681
+ *
682
+ * @throws AlgoliaException
683
+ */
684
+ public function getSettings()
685
+ {
686
+ return $this->client->request(
687
+ $this->context,
688
+ 'GET',
689
+ '/1/indexes/'.$this->urlIndexName.'/settings',
690
+ null,
691
+ null,
692
+ $this->context->readHostsArray,
693
+ $this->context->connectTimeout,
694
+ $this->context->readTimeout
695
+ );
696
+ }
697
+
698
+ /**
699
  * This function deletes the index content. Settings and index specific API keys are kept untouched.
700
+ *
701
+ * @return mixed
702
+ *
703
+ * @throws AlgoliaException
704
  */
705
+ public function clearIndex()
706
+ {
707
+ return $this->client->request(
708
+ $this->context,
709
+ 'POST',
710
+ '/1/indexes/'.$this->urlIndexName.'/clear',
711
+ null,
712
+ null,
713
+ $this->context->writeHostsArray,
714
+ $this->context->connectTimeout,
715
+ $this->context->readTimeout
716
+ );
717
  }
718
 
719
+ /**
720
+ * Set settings for this index.
721
+ *
722
+ * @param mixed $settings the settings object that can contains :
723
+ * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default =
724
+ * 3).
725
+ * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default
726
+ * = 7).
727
+ * - hitsPerPage: (integer) the number of hits per page (default = 10).
728
+ * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
729
+ * If set to null, all attributes are retrieved.
730
+ * - attributesToHighlight: (array of strings) default list of attributes to highlight.
731
+ * If set to null, all indexed attributes are highlighted.
732
+ * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the
733
+ * number of words to return (syntax is attributeName:nbWords). By default no snippet is computed.
734
+ * If set to null, no snippet is computed.
735
+ * - attributesToIndex: (array of strings) the list of fields you want to index.
736
+ * If set to null, all textual and numerical attributes of your objects are indexed, but you
737
+ * should update it to get optimal results. This parameter has two important uses:
738
+ * - Limit the attributes to index: For example if you store a binary image in base64, you want to
739
+ * store it and be able to retrieve it but you don't want to search in the base64 string.
740
+ * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in
741
+ * attributes at the beginning of the list will be considered more important than matches in
742
+ * attributes further down the list. In one attribute, matching text at the beginning of the
743
+ * attribute will be considered more important than text after, you can disable this behavior if
744
+ * you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex:
745
+ * ["title", "unordered(text)"].
746
+ * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
747
+ * All strings in the attribute selected for faceting are extracted and added as a facet. If set
748
+ * to null, no attribute is used for faceting.
749
+ * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature
750
+ * is similar to the SQL "distinct" keyword: when enabled in query with the distinct=1 parameter,
751
+ * all hits containing a duplicate value for this attribute are removed from results. For example,
752
+ * if the chosen attribute is show_name and several hits have the same value for show_name, then
753
+ * only the best one is kept and others are removed.
754
+ * - ranking: (array of strings) controls the way results are sorted.
755
+ * We have six available criteria:
756
+ * - typo: sort according to number of typos,
757
+ * - geo: sort according to decreassing distance when performing a geo-location based search,
758
+ * - proximity: sort according to the proximity of query words in hits,
759
+ * - attribute: sort according to the order of attributes defined by attributesToIndex,
760
+ * - exact:
761
+ * - if the user query contains one word: sort objects having an attribute that is exactly the
762
+ * query word before others. For example if you search for the "V" TV show, you want to find it
763
+ * with the "V" query and avoid to have all popular TV show starting by the v letter before it.
764
+ * - if the user query contains multiple words: sort according to the number of words that matched
765
+ * exactly (and not as a prefix).
766
+ * - custom: sort according to a user defined formula set in **customRanking** attribute.
767
+ * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
768
+ * - customRanking: (array of strings) lets you specify part of the ranking.
769
+ * The syntax of this condition is an array of strings containing attributes prefixed by asc
770
+ * (ascending order) or desc (descending order) operator. For example `"customRanking" =>
771
+ * ["desc(population)", "asc(name)"]`
772
+ * - queryType: Select how the query words are interpreted, it can be one of the following value:
773
+ * - prefixAll: all query words are interpreted as prefixes,
774
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
775
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
776
+ * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in
777
+ * the query result (default to "<em>").
778
+ * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in
779
+ * the query result (default to "</em>").
780
+ * - optionalWords: (array of strings) Specify a list of words that should be considered as
781
+ * optional when found in the query.
782
+ *
783
+ * @return mixed
784
+ */
785
+ public function setSettings($settings)
786
+ {
787
+ return $this->client->request(
788
+ $this->context,
789
+ 'PUT',
790
+ '/1/indexes/'.$this->urlIndexName.'/settings',
791
+ [],
792
+ $settings,
793
+ $this->context->writeHostsArray,
794
+ $this->context->connectTimeout,
795
+ $this->context->readTimeout
796
+ );
797
  }
798
 
799
+ /**
800
+ * List all existing user keys associated to this index with their associated ACLs.
801
  *
802
+ * @return mixed
803
+ *
804
+ * @throws AlgoliaException
805
  */
806
+ public function listUserKeys()
807
+ {
808
+ return $this->client->request(
809
+ $this->context,
810
+ 'GET',
811
+ '/1/indexes/'.$this->urlIndexName.'/keys',
812
+ null,
813
+ null,
814
+ $this->context->readHostsArray,
815
+ $this->context->connectTimeout,
816
+ $this->context->readTimeout
817
+ );
818
  }
819
 
820
+ /**
821
+ * Get ACL of a user key associated to this index.
822
  *
823
+ * @param string $key
824
+ *
825
+ * @return mixed
826
+ *
827
+ * @throws AlgoliaException
828
  */
829
+ public function getUserKeyACL($key)
830
+ {
831
+ return $this->client->request(
832
+ $this->context,
833
+ 'GET',
834
+ '/1/indexes/'.$this->urlIndexName.'/keys/'.$key,
835
+ null,
836
+ null,
837
+ $this->context->readHostsArray,
838
+ $this->context->connectTimeout,
839
+ $this->context->readTimeout
840
+ );
841
  }
842
 
843
+ /**
844
+ * Delete an existing user key associated to this index.
845
+ *
846
+ * @param string $key
847
  *
848
+ * @return mixed
849
+ *
850
+ * @throws AlgoliaException
851
  */
852
+ public function deleteUserKey($key)
853
+ {
854
+ return $this->client->request(
855
+ $this->context,
856
+ 'DELETE',
857
+ '/1/indexes/'.$this->urlIndexName.'/keys/'.$key,
858
+ null,
859
+ null,
860
+ $this->context->writeHostsArray,
861
+ $this->context->connectTimeout,
862
+ $this->context->readTimeout
863
+ );
864
  }
865
 
866
+ /**
867
+ * Create a new user key associated to this index.
868
+ *
869
+ * @param array $obj can be two different parameters:
870
+ * The list of parameters for this key. Defined by a array that
871
+ * can contains the following values:
872
+ * - acl: array of string
873
+ * - indices: array of string
874
+ * - validity: int
875
+ * - referers: array of string
876
+ * - description: string
877
+ * - maxHitsPerQuery: integer
878
+ * - queryParameters: string
879
+ * - maxQueriesPerIPPerHour: integer
880
+ * Or the list of ACL for this key. Defined by an array of NSString that
881
+ * can contains the following values:
882
+ * - search: allow to search (https and http)
883
+ * - addObject: allows to add/update an object in the index (https only)
884
+ * - deleteObject : allows to delete an existing object (https only)
885
+ * - deleteIndex : allows to delete index content (https only)
886
+ * - settings : allows to get index settings (https only)
887
+ * - editSettings : allows to change index settings (https only)
888
+ * @param int $validity the number of seconds after which the key will be automatically removed (0 means
889
+ * no time limit for this key)
890
+ * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
891
+ * Defaults to 0 (no rate limit).
892
+ * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
893
+ * Defaults to 0 (unlimited)
894
+ *
895
+ * @return mixed
896
+ *
897
+ * @throws AlgoliaException
898
+ */
899
+ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0)
900
+ {
901
+ // is dict of value
902
+ if ($obj !== array_values($obj)) {
903
  $params = $obj;
904
+ $params['validity'] = $validity;
905
+ $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour;
906
+ $params['maxHitsPerQuery'] = $maxHitsPerQuery;
907
  } else {
908
+ $params = [
909
+ 'acl' => $obj,
910
+ 'validity' => $validity,
911
+ 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour,
912
+ 'maxHitsPerQuery' => $maxHitsPerQuery,
913
+ ];
914
  }
915
+
916
+ return $this->client->request(
917
+ $this->context,
918
+ 'POST',
919
+ '/1/indexes/'.$this->urlIndexName.'/keys',
920
+ [],
921
+ $params,
922
+ $this->context->writeHostsArray,
923
+ $this->context->connectTimeout,
924
+ $this->context->readTimeout
925
+ );
926
  }
927
 
928
+ /**
929
+ * Update a user key associated to this index.
930
+ *
931
+ * @param string $key
932
+ * @param array $obj can be two different parameters:
933
+ * The list of parameters for this key. Defined by a array that
934
+ * can contains the following values:
935
+ * - acl: array of string
936
+ * - indices: array of string
937
+ * - validity: int
938
+ * - referers: array of string
939
+ * - description: string
940
+ * - maxHitsPerQuery: integer
941
+ * - queryParameters: string
942
+ * - maxQueriesPerIPPerHour: integer
943
+ * Or the list of ACL for this key. Defined by an array of NSString that
944
+ * can contains the following values:
945
+ * - search: allow to search (https and http)
946
+ * - addObject: allows to add/update an object in the index (https only)
947
+ * - deleteObject : allows to delete an existing object (https only)
948
+ * - deleteIndex : allows to delete index content (https only)
949
+ * - settings : allows to get index settings (https only)
950
+ * - editSettings : allows to change index settings (https only)
951
+ * @param int $validity the number of seconds after which the key will be automatically removed (0 means
952
+ * no time limit for this key)
953
+ * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
954
+ * Defaults to 0 (no rate limit).
955
+ * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
956
+ * Defaults to 0 (unlimited)
957
+ *
958
+ * @return mixed
959
+ *
960
+ * @throws AlgoliaException
961
+ */
962
+ public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0)
963
+ {
964
+ // is dict of value
965
+ if ($obj !== array_values($obj)) {
966
  $params = $obj;
967
+ $params['validity'] = $validity;
968
+ $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour;
969
+ $params['maxHitsPerQuery'] = $maxHitsPerQuery;
970
  } else {
971
+ $params = [
972
+ 'acl' => $obj,
973
+ 'validity' => $validity,
974
+ 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour,
975
+ 'maxHitsPerQuery' => $maxHitsPerQuery,
976
+ ];
977
  }
978
+
979
+ return $this->client->request(
980
+ $this->context,
981
+ 'PUT',
982
+ '/1/indexes/'.$this->urlIndexName.'/keys/'.$key,
983
+ [],
984
+ $params,
985
+ $this->context->writeHostsArray,
986
+ $this->context->connectTimeout,
987
+ $this->context->readTimeout
988
+ );
989
  }
990
 
991
  /**
992
+ * Send a batch request.
993
+ *
994
+ * @param array $requests an associative array defining the batch request body
995
+ *
996
+ * @return mixed
997
  */
998
+ public function batch($requests)
999
+ {
1000
+ return $this->client->request(
1001
+ $this->context,
1002
+ 'POST',
1003
+ '/1/indexes/'.$this->urlIndexName.'/batch',
1004
+ [],
1005
+ $requests,
1006
+ $this->context->writeHostsArray,
1007
+ $this->context->connectTimeout,
1008
+ $this->context->readTimeout
1009
+ );
1010
  }
1011
 
1012
  /**
1013
+ * Build a batch request.
1014
+ *
1015
+ * @param string $action the batch action
1016
+ * @param array $objects the array of objects
1017
+ * @param string $withObjectID set an 'objectID' attribute
1018
+ * @param string $objectIDKey the objectIDKey
1019
+ *
1020
  * @return array
1021
  */
1022
+ private function buildBatch($action, $objects, $withObjectID, $objectIDKey = 'objectID')
1023
+ {
1024
+ $requests = [];
1025
  foreach ($objects as $obj) {
1026
+ $req = ['action' => $action, 'body' => $obj];
1027
  if ($withObjectID && array_key_exists($objectIDKey, $obj)) {
1028
+ $req['objectID'] = (string) $obj[$objectIDKey];
1029
  }
1030
  array_push($requests, $req);
1031
  }
1032
+
1033
+ return ['requests' => $requests];
1034
+ }
1035
+
1036
+ /**
1037
+ * @param string $query
1038
+ * @param array|null $params
1039
+ *
1040
+ * @return IndexBrowser
1041
+ */
1042
+ private function doBrowse($query, $params = null)
1043
+ {
1044
+ return new IndexBrowser($this, $query, $params);
1045
+ }
1046
+
1047
+ /**
1048
+ * @param string $query
1049
+ * @param array|null $params
1050
+ * @param $cursor
1051
+ *
1052
+ * @return mixed
1053
+ */
1054
+ public function browseFrom($query, $params = null, $cursor = null)
1055
+ {
1056
+ if ($params === null) {
1057
+ $params = [];
1058
+ }
1059
+ foreach ($params as $key => $value) {
1060
+ if (gettype($value) == 'array') {
1061
+ $params[$key] = json_encode($value);
1062
+ }
1063
+ }
1064
+ if ($query != null) {
1065
+ $params['query'] = $query;
1066
+ }
1067
+ if ($cursor != null) {
1068
+ $params['cursor'] = $cursor;
1069
+ }
1070
+
1071
+ return $this->client->request(
1072
+ $this->context,
1073
+ 'GET',
1074
+ '/1/indexes/'.$this->urlIndexName.'/browse',
1075
+ $params,
1076
+ null,
1077
+ $this->context->readHostsArray,
1078
+ $this->context->connectTimeout,
1079
+ $this->context->readTimeout
1080
+ );
1081
+ }
1082
+
1083
+ /**
1084
+ * @param string $name
1085
+ * @param array $arguments
1086
+ *
1087
+ * @return mixed
1088
+ */
1089
+ public function __call($name, $arguments)
1090
+ {
1091
+ if ($name !== 'browse') {
1092
+ return;
1093
+ }
1094
+
1095
+ if (count($arguments) >= 1 && is_string($arguments[0])) {
1096
+ return call_user_func_array([$this, 'doBrowse'], $arguments);
1097
+ }
1098
+
1099
+ return call_user_func_array([$this, 'doBcBrowse'], $arguments);
1100
  }
1101
  }
lib/AlgoliaSearch/IndexBrowser.php ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Copyright (c) 2013 Algolia
5
+ * http://www.algolia.com/
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ *
26
+ */
27
+
28
+ namespace AlgoliaSearch;
29
+
30
+ class IndexBrowser implements \Iterator
31
+ {
32
+ /**
33
+ * @var string
34
+ */
35
+ private $query;
36
+
37
+ /**
38
+ * @var int
39
+ */
40
+ private $position;
41
+
42
+ /**
43
+ * @var array
44
+ */
45
+ private $hit;
46
+
47
+ /**
48
+ * @var array
49
+ */
50
+ private $params;
51
+
52
+ /**
53
+ * @var array
54
+ */
55
+ private $answer;
56
+
57
+ /**
58
+ * @var Index
59
+ */
60
+ private $index;
61
+
62
+ /**
63
+ * @var int
64
+ */
65
+ private $cursor;
66
+
67
+ /**
68
+ * IndexBrowser constructor.
69
+ *
70
+ * @param Index $index
71
+ * @param string $query
72
+ * @param array|null $params
73
+ * @param int|null $cursor
74
+ */
75
+ public function __construct(Index $index, $query, $params = null, $cursor = null)
76
+ {
77
+ $this->index = $index;
78
+ $this->query = $query;
79
+ $this->params = $params;
80
+
81
+ $this->position = 0;
82
+
83
+ $this->doQuery($cursor);
84
+ }
85
+
86
+ /**
87
+ * @return mixed
88
+ */
89
+ public function current()
90
+ {
91
+ return $this->hit;
92
+ }
93
+
94
+ /**
95
+ * @return mixed
96
+ */
97
+ public function next()
98
+ {
99
+ return $this->hit;
100
+ }
101
+
102
+ /**
103
+ * @return int
104
+ */
105
+ public function key()
106
+ {
107
+ return $this->position;
108
+ }
109
+
110
+ /**
111
+ * @return bool
112
+ */
113
+ public function valid()
114
+ {
115
+ do {
116
+ if ($this->position < count($this->answer['hits'])) {
117
+ $this->hit = $this->answer['hits'][$this->position];
118
+ $this->position++;
119
+
120
+ return true;
121
+ }
122
+
123
+ if (isset($this->answer['cursor']) && $this->answer['cursor']) {
124
+ $this->position = 0;
125
+
126
+ $this->doQuery($this->answer['cursor']);
127
+
128
+ continue;
129
+ }
130
+
131
+ return false;
132
+ } while (true);
133
+ }
134
+
135
+ public function rewind()
136
+ {
137
+ $this->cursor = null;
138
+ $this->position = 0;
139
+ }
140
+
141
+ /**
142
+ * @return int
143
+ */
144
+ public function cursor()
145
+ {
146
+ return $this->answer['cursor'];
147
+ }
148
+
149
+ /**
150
+ * @param int $cursor
151
+ */
152
+ private function doQuery($cursor = null)
153
+ {
154
+ if ($cursor !== null) {
155
+ $this->params['cursor'] = $cursor;
156
+ }
157
+
158
+ $this->answer = $this->index->browseFrom($this->query, $this->params, $cursor);
159
+ }
160
+ }
lib/AlgoliaSearch/PlacesIndex.php ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Copyright (c) 2013 Algolia
5
+ * http://www.algolia.com/
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ *
26
+ */
27
+
28
+ namespace AlgoliaSearch;
29
+
30
+ class PlacesIndex
31
+ {
32
+ private $context;
33
+ private $client;
34
+
35
+ /**
36
+ * @param ClientContext $context
37
+ * @param Client $client
38
+ */
39
+ public function __construct($context, Client $client)
40
+ {
41
+ $this->context = $context;
42
+ $this->client = $client;
43
+ }
44
+
45
+ /**
46
+ * @param string $query
47
+ * @param array|null $args
48
+ *
49
+ * @return mixed
50
+ *
51
+ * @throws AlgoliaException
52
+ */
53
+ public function search($query, $args = null)
54
+ {
55
+ if ($args === null) {
56
+ $args = [];
57
+ }
58
+ $args['query'] = $query;
59
+
60
+ return $this->client->request(
61
+ $this->context,
62
+ 'POST',
63
+ '/1/places/query',
64
+ [],
65
+ ['params' => $this->client->buildQuery($args)],
66
+ $this->context->readHostsArray,
67
+ $this->context->connectTimeout,
68
+ $this->context->searchTimeout
69
+ );
70
+ }
71
+
72
+ public function setExtraHeader($key, $value)
73
+ {
74
+ $this->context->setExtraHeader($key, $value);
75
+ }
76
+
77
+ public function getContext()
78
+ {
79
+ return $this->context;
80
+ }
81
+ }
lib/AlgoliaSearch/Version.php CHANGED
@@ -1,4 +1,5 @@
1
  <?php
 
2
  /*
3
  * Copyright (c) 2013 Algolia
4
  * http://www.algolia.com/
@@ -23,13 +24,14 @@
23
  *
24
  *
25
  */
 
26
  namespace AlgoliaSearch;
27
 
28
  class Version
29
  {
30
- const VALUE = "1.5.6";
31
 
32
- public static $custom_value = "";
33
 
34
  public static function get()
35
  {
1
  <?php
2
+
3
  /*
4
  * Copyright (c) 2013 Algolia
5
  * http://www.algolia.com/
24
  *
25
  *
26
  */
27
+
28
  namespace AlgoliaSearch;
29
 
30
  class Version
31
  {
32
+ const VALUE = '1.8.0';
33
 
34
+ public static $custom_value = '';
35
 
36
  public static function get()
37
  {
package.xml CHANGED
@@ -1,7 +1,7 @@
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>algoliasearch</name>
4
- <version>1.5.4</version>
5
  <stability>stable</stability>
6
  <license uri="https://github.com/algolia/algoliasearch-magento/blob/master/LICENSE.txt">MIT</license>
7
  <channel>community</channel>
@@ -11,14 +11,20 @@
11
  &#xD;
12
  This extension replaces Magento's FullText Search module and provide an as-you-type auto-completion menu in your searchbar.&#xD;
13
  </description>
14
- <notes>- UPDATED: instantsearch.js update&#xD;
15
- - FIX: issue with slaves creation&#xD;
16
- - FIX: issue for bundle products when customer group is enabled&#xD;
17
- - FIX: casting in resulting in bad typing in Algolia</notes>
 
 
 
 
 
 
18
  <authors><author><name>Algolia Team</name><user>algolia</user><email>support@algolia.com</email></author></authors>
19
- <date>2016-03-10</date>
20
- <time>10:22:27</time>
21
- <contents><target name="mageetc"><dir name="modules"><file name="Algolia_Algoliasearch.xml" hash="86f9f0ffb3d018dab8231c49901fe36e"/></dir></target><target name="magecommunity"><dir name="Algolia"><dir name="Algoliasearch"><dir name="Block"><dir name="System"><dir name="Config"><dir name="Form"><dir name="Field"><file name="Custompages.php" hash="f87a9cf7b5559717cd9d6570374dcda7"/><file name="Customrankingcategory.php" hash="6d9575c12dbaecf9054de1cf12736025"/><file name="Customrankingproduct.php" hash="6d1b145e37c4f22d5b56f5783ac47511"/><file name="Customsortorder.php" hash="786c8f8fca2e4b41b8732f5fe270491b"/><file name="Customsortordercategory.php" hash="9908ea7f463138d3047c51b98591db9c"/><file name="Customsortorderproduct.php" hash="ee62901a3911bb7784467e1ca5cd8e84"/><file name="Facets.php" hash="b8c6217811a1c9afd64119d2b021cc5f"/><file name="Sections.php" hash="7aa62da4fb45f693bf81ad4aa0421ac3"/><file name="Select.php" hash="6e3cb4c1798775048bebbdc878e90aa9"/><file name="Sorts.php" hash="fede73c4ecbe39bf0344fbf6de46ed95"/></dir></dir></dir></dir></dir><dir name="Helper"><file name="Algoliahelper.php" hash="100ba0799077cb39ad868c08e162014f"/><file name="Config.php" hash="83ed8c8c3791806c074c247ebeaa30e4"/><file name="Data.php" hash="2da928c02c463c16fd3f527c384df18a"/><dir name="Entity"><file name="Additionalsectionshelper.php" hash="d4f67429e539e9ee9a5365ba45808770"/><file name="Categoryhelper.php" hash="5f3a80ddd98dd190148d47c3946ec9e7"/><file name="Helper.php" hash="c076e3caa22d5e98df9773cc8c519a8a"/><file name="Pagehelper.php" hash="b9ccdbdc677eae9442741f96d648d991"/><file name="Producthelper.php" hash="fce516ead05612b2dd37667110cf29ce"/><file name="Suggestionhelper.php" hash="6bbc72c1bbc17b13f8d9be1e0bb46fa2"/></dir><file name="Image.php" hash="876292c0612fa87194d1657a7facb916"/><file name="Logger.php" hash="0431f992b1cda07a0c552392fc67d89b"/></dir><dir name="Model"><dir name="Indexer"><file name="Algolia.php" hash="9401b46d5c57b38fe97c5b28c7cf8cc2"/><file name="Algoliaadditionalsections.php" hash="649d4468a899f055d33dc959e758a688"/><file name="Algoliacategories.php" hash="7dda3999434d4fd44f11327240621e25"/><file name="Algoliapages.php" hash="22230967d949c89f92a794bd46fc6fa9"/><file name="Algoliaqueuerunner.php" hash="dbad890d433eee91732f44e36b6ac7e2"/><file name="Algoliasuggestions.php" hash="7a6702e8299f646356749fed8f57ca0a"/></dir><file name="Observer.php" hash="73ee1e9eb227085407bd130a4665a3ab"/><file name="Queue.php" hash="c3ec68c4441305ff9acc033d123a7823"/><dir name="Resource"><file name="Engine.php" hash="6030a8dbb933ce9f1c423ec1ef078f9d"/><dir name="Fulltext"><file name="Collection.php" hash="cf6c1b8ecaea31619db8186ccae8a2d4"/></dir><file name="Fulltext.php" hash="014ac885c31abeaa8e8ec56bbe54ff3d"/></dir><dir name="System"><file name="Imagetype.php" hash="5fd4dbd98818a15b0253c5988a65a785"/><file name="Removewords.php" hash="25408eb3e3d278da2f2ec1a6b6e6d8e8"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="ea4176ed43885e531f90d1f5369f29ee"/><file name="config.xml" hash="90606b2afb274b2d0a59f4fafec3eb5f"/><file name="system.xml" hash="d03a2af78a950c20da77bac886c88a04"/></dir><dir name="sql"><dir name="algoliasearch_setup"><file name="mysql4-install-0.1.0.php" hash="fffd964f9c60be7909ec216260c37ba0"/><file name="mysql4-upgrade-0.1.0-1.4.8.php" hash="5224f8f1031a0659c64d393392a7f199"/><file name="mysql4-upgrade-1.4.8-1.5.0.php" hash="fa8be181b2d43e955e75dce1ed4a19ca"/></dir></dir></dir></dir></target><target name="magedesign"><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="algoliasearch.xml" hash="28b9676bd76adfdbb9eecb54ce9c5f02"/></dir><dir name="template"><dir name="algoliasearch"><file name="beforecontent.phtml" hash="19f2ee9532f4e46c77ade0157976b780"/><file name="beforetopsearch.phtml" hash="7561e3dd68ea0a014e636fd572f4bad3"/><file name="frontjs.phtml" hash="7d2cdf7bb5c2f47c0f118eac0ba1ead1"/><file name="topsearch.phtml" hash="724abb8b9b5d4276492557f48492e05f"/></dir></dir></dir></dir></dir><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="template"><dir name="algoliasearch"><file name="adminjs.phtml" hash="a5842a5c608c1496060dd4610c5c774f"/></dir></dir><dir name="layout"><file name="algoliasearch.xml" hash="312ecb88cb4ae694d098bda8f580d29e"/></dir></dir></dir></dir></target><target name="mageskin"><dir name="frontend"><dir name="base"><dir name="default"><dir name="algoliasearch"><file name="algolia-admin-menu.png" hash="9202a559c30a43d4d4bbc2f9ee774fd9"/><file name="algolia-logo.png" hash="190884b3e8652f3517754ae15bca31de"/><file name="algoliasearch.css" hash="d7c32abcd1151b88c156bd92253ec5f9"/><file name="cross-circle.png" hash="a9ae2fa7ec458ffaf7c32613ca9593da"/><file name="cross.png" hash="a046cd95cba9761c824063fbd30a26b5"/><dir name="images"><file name="ui-bg_diagonals-thick_18_b81900_40x40.png" hash="62568c006bb1066f40fd5f9cfe4489be"/><file name="ui-bg_diagonals-thick_20_666666_40x40.png" hash="406541454ec466d93217826588335194"/><file name="ui-bg_flat_10_000000_40x100.png" hash="85243ed808c91ae60d33bda3a6bdee3c"/><file name="ui-bg_glass_100_f6f6f6_1x400.png" hash="f912ffca9b1919ab26c64cf1332c5322"/><file name="ui-bg_glass_100_fdf5ce_1x400.png" hash="a9b41e3f4db0fb9be1cd2c649deb253f"/><file name="ui-bg_glass_65_ffffff_1x400.png" hash="ff9e9b45e03f11808144324fd5350612"/><file name="ui-bg_gloss-wave_35_f6a828_500x100.png" hash="08ece8908c07b1c0d18b8db076ff50fc"/><file name="ui-bg_highlight-soft_100_eeeeee_1x100.png" hash="72fe4b0e1bbb83dfd6787989d3583fbe"/><file name="ui-bg_highlight-soft_75_ffe45c_1x100.png" hash="81262299ac7f591fd1763c1ccee0691f"/><file name="ui-icons_222222_256x240.png" hash="3a3c5468f484f07ac4a320d9e22acb8c"/><file name="ui-icons_228ef1_256x240.png" hash="92b29683b6a48eae7de7eb4b1cfa039c"/><file name="ui-icons_ef8c08_256x240.png" hash="f492970693640894fb54166c75dd2925"/><file name="ui-icons_ffd27a_256x240.png" hash="dda1b6f694b0d196aefc66a1d6d758f6"/><file name="ui-icons_ffffff_256x240.png" hash="41612b0f4a034424f8321c9f824a94da"/></dir></dir></dir></dir></dir></target><target name="mage"><dir name="js"><dir name="algoliasearch"><file name="Function.prototype.bind.js" hash="eb15975feb0cc976face88cb194294ae"/><file name="admin_scripts.js" hash="877a9fcbc5d3d627772464a9311ae0b3"/><file name="algoliaAdminBundle.min.js" hash="26d145a6c347021083bc140a0d07f0bb"/><file name="algoliaAdminBundle.min.js.map" hash="39d5d78a949a11321a596561d95ef01e"/><file name="algoliaBundle.min.js" hash="a5cf86fa138eccdd8aa3d39b170a1687"/><file name="algoliaBundle.min.js.map" hash="95baad19b0b4f8de10eee092ef060174"/></dir></dir><dir name="lib"><dir name="AlgoliaSearch"><file name="AlgoliaException.php" hash="4acaa7c9142e19d1084295a3b8ba18e2"/><file name="Client.php" hash="11ad687a9868a9f574ae6a069800dd2c"/><file name="ClientContext.php" hash="77d2449636d263162460a7ccaea4e6b6"/><file name="Index.php" hash="5c1eacc54cd503bff296e9bbbd402895"/><file name="Version.php" hash="0c37eb6324361991364e0efd2696e56d"/><dir name="resources"><file name="ca-bundle.crt" hash="47961e7ef15667c93cd99be01b51f00a"/></dir></dir></dir></target></contents>
22
  <compatible/>
23
  <dependencies><required><php><min>5.2.0</min><max>6.0.0</max></php></required></dependencies>
24
  </package>
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>algoliasearch</name>
4
+ <version>1.5.5</version>
5
  <stability>stable</stability>
6
  <license uri="https://github.com/algolia/algoliasearch-magento/blob/master/LICENSE.txt">MIT</license>
7
  <channel>community</channel>
11
  &#xD;
12
  This extension replaces Magento's FullText Search module and provide an as-you-type auto-completion menu in your searchbar.&#xD;
13
  </description>
14
+ <notes>- NEW: Add an option to include data from out-of-stock sub products&#xD;
15
+ - NEW: Use secured api keys to only retrieve one group price in the frontend&#xD;
16
+ - NEW: Better update strategy to simplify the indexer code and to avoid missing deleted products event&#xD;
17
+ - UPDATE: Better handling of include in navigation config&#xD;
18
+ - UPDATE: underlying php client&#xD;
19
+ - UPDATE: Conditionally render template directives&#xD;
20
+ - UPDATE: Make sub product skus searchable&#xD;
21
+ - FIX: slaves creation issue&#xD;
22
+ - FIX: small price issue&#xD;
23
+ - FIX: fallback to default search in case there is a error from the api</notes>
24
  <authors><author><name>Algolia Team</name><user>algolia</user><email>support@algolia.com</email></author></authors>
25
+ <date>2016-04-28</date>
26
+ <time>15:22:48</time>
27
+ <contents><target name="mageetc"><dir name="modules"><file name="Algolia_Algoliasearch.xml" hash="9e57a7153901c3302833296623bec4d7"/></dir></target><target name="magecommunity"><dir name="Algolia"><dir name="Algoliasearch"><dir name="Block"><dir name="System"><dir name="Config"><dir name="Form"><dir name="Field"><file name="Custompages.php" hash="f87a9cf7b5559717cd9d6570374dcda7"/><file name="Customrankingcategory.php" hash="6d9575c12dbaecf9054de1cf12736025"/><file name="Customrankingproduct.php" hash="6d1b145e37c4f22d5b56f5783ac47511"/><file name="Customsortorder.php" hash="786c8f8fca2e4b41b8732f5fe270491b"/><file name="Customsortordercategory.php" hash="9908ea7f463138d3047c51b98591db9c"/><file name="Customsortorderproduct.php" hash="ee62901a3911bb7784467e1ca5cd8e84"/><file name="Facets.php" hash="b8c6217811a1c9afd64119d2b021cc5f"/><file name="Sections.php" hash="7aa62da4fb45f693bf81ad4aa0421ac3"/><file name="Select.php" hash="6e3cb4c1798775048bebbdc878e90aa9"/><file name="Sorts.php" hash="fede73c4ecbe39bf0344fbf6de46ed95"/></dir></dir></dir></dir></dir><dir name="Helper"><file name="Algoliahelper.php" hash="19d6a822a08a73a16506bd37062c226a"/><file name="Config.php" hash="63fd2579161ad0c57e914f27f36710e6"/><file name="Data.php" hash="e6732c8c949bc77ec5172552a3d473f8"/><dir name="Entity"><file name="Additionalsectionshelper.php" hash="a543c603d7a5ad19e5cade0ff569c829"/><file name="Categoryhelper.php" hash="f67337a249b04028de1fafc9acf83cfd"/><file name="Helper.php" hash="39eca180885e5ed15d7a7ffbb1cae297"/><file name="Pagehelper.php" hash="4c92750f40f3fc86f470e758e0cade75"/><file name="Producthelper.php" hash="a4281d4f43acd49879ed8e9c60739c3c"/><file name="Suggestionhelper.php" hash="b3eaf3b253376b2387dfaf65570bc5e8"/></dir><file name="Image.php" hash="876292c0612fa87194d1657a7facb916"/><file name="Logger.php" hash="08bc6a4e377b60ca13b96a7982a8fcb5"/></dir><dir name="Model"><dir name="Indexer"><file name="Algolia.php" hash="64a3247da61c4adb4e07e8e1e4863fcf"/><file name="Algoliaadditionalsections.php" hash="649d4468a899f055d33dc959e758a688"/><file name="Algoliacategories.php" hash="8917dcd32ba581c8a5477f373ec17268"/><file name="Algoliapages.php" hash="22230967d949c89f92a794bd46fc6fa9"/><file name="Algoliaqueuerunner.php" hash="dbad890d433eee91732f44e36b6ac7e2"/><file name="Algoliasuggestions.php" hash="7a6702e8299f646356749fed8f57ca0a"/></dir><file name="Observer.php" hash="73ee1e9eb227085407bd130a4665a3ab"/><file name="Queue.php" hash="c3ec68c4441305ff9acc033d123a7823"/><dir name="Resource"><file name="Engine.php" hash="6030a8dbb933ce9f1c423ec1ef078f9d"/><dir name="Fulltext"><file name="Collection.php" hash="e75eea1119840095486710d438d9ca1b"/></dir><file name="Fulltext.php" hash="014ac885c31abeaa8e8ec56bbe54ff3d"/></dir><dir name="System"><file name="Imagetype.php" hash="5fd4dbd98818a15b0253c5988a65a785"/><file name="Removewords.php" hash="25408eb3e3d278da2f2ec1a6b6e6d8e8"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="ea4176ed43885e531f90d1f5369f29ee"/><file name="config.xml" hash="3caf38ac6af0159e4d3a9157e8c78ccd"/><file name="system.xml" hash="2d26a885e9e72f5687f2e4b6cabe0a2d"/></dir><dir name="sql"><dir name="algoliasearch_setup"><file name="mysql4-install-0.1.0.php" hash="fffd964f9c60be7909ec216260c37ba0"/><file name="mysql4-upgrade-0.1.0-1.4.8.php" hash="5224f8f1031a0659c64d393392a7f199"/><file name="mysql4-upgrade-1.4.8-1.5.0.php" hash="fa8be181b2d43e955e75dce1ed4a19ca"/></dir></dir></dir></dir></target><target name="magedesign"><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="algoliasearch.xml" hash="28b9676bd76adfdbb9eecb54ce9c5f02"/></dir><dir name="template"><dir name="algoliasearch"><file name="beforecontent.phtml" hash="19f2ee9532f4e46c77ade0157976b780"/><file name="beforetopsearch.phtml" hash="d4d1444b59e94e6ee7680aa922130390"/><file name="frontjs.phtml" hash="7d2cdf7bb5c2f47c0f118eac0ba1ead1"/><file name="topsearch.phtml" hash="1647543f31b419380c15b48817e5bfb3"/></dir></dir></dir></dir></dir><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="template"><dir name="algoliasearch"><file name="adminjs.phtml" hash="a5842a5c608c1496060dd4610c5c774f"/></dir></dir><dir name="layout"><file name="algoliasearch.xml" hash="312ecb88cb4ae694d098bda8f580d29e"/></dir></dir></dir></dir></target><target name="mageskin"><dir name="frontend"><dir name="base"><dir name="default"><dir name="algoliasearch"><file name="algolia-admin-menu.png" hash="9202a559c30a43d4d4bbc2f9ee774fd9"/><file name="algolia-logo.png" hash="190884b3e8652f3517754ae15bca31de"/><file name="algoliasearch.css" hash="d7c32abcd1151b88c156bd92253ec5f9"/><file name="cross-circle.png" hash="a9ae2fa7ec458ffaf7c32613ca9593da"/><file name="cross.png" hash="a046cd95cba9761c824063fbd30a26b5"/><dir name="images"><file name="ui-bg_diagonals-thick_18_b81900_40x40.png" hash="62568c006bb1066f40fd5f9cfe4489be"/><file name="ui-bg_diagonals-thick_20_666666_40x40.png" hash="406541454ec466d93217826588335194"/><file name="ui-bg_flat_10_000000_40x100.png" hash="85243ed808c91ae60d33bda3a6bdee3c"/><file name="ui-bg_glass_100_f6f6f6_1x400.png" hash="f912ffca9b1919ab26c64cf1332c5322"/><file name="ui-bg_glass_100_fdf5ce_1x400.png" hash="a9b41e3f4db0fb9be1cd2c649deb253f"/><file name="ui-bg_glass_65_ffffff_1x400.png" hash="ff9e9b45e03f11808144324fd5350612"/><file name="ui-bg_gloss-wave_35_f6a828_500x100.png" hash="08ece8908c07b1c0d18b8db076ff50fc"/><file name="ui-bg_highlight-soft_100_eeeeee_1x100.png" hash="72fe4b0e1bbb83dfd6787989d3583fbe"/><file name="ui-bg_highlight-soft_75_ffe45c_1x100.png" hash="81262299ac7f591fd1763c1ccee0691f"/><file name="ui-icons_222222_256x240.png" hash="3a3c5468f484f07ac4a320d9e22acb8c"/><file name="ui-icons_228ef1_256x240.png" hash="92b29683b6a48eae7de7eb4b1cfa039c"/><file name="ui-icons_ef8c08_256x240.png" hash="f492970693640894fb54166c75dd2925"/><file name="ui-icons_ffd27a_256x240.png" hash="dda1b6f694b0d196aefc66a1d6d758f6"/><file name="ui-icons_ffffff_256x240.png" hash="41612b0f4a034424f8321c9f824a94da"/></dir></dir></dir></dir></dir></target><target name="mage"><dir name="js"><dir name="algoliasearch"><file name="Function.prototype.bind.js" hash="eb15975feb0cc976face88cb194294ae"/><file name="admin_scripts.js" hash="877a9fcbc5d3d627772464a9311ae0b3"/><file name="algoliaAdminBundle.min.js" hash="26d145a6c347021083bc140a0d07f0bb"/><file name="algoliaAdminBundle.min.js.map" hash="39d5d78a949a11321a596561d95ef01e"/><file name="algoliaBundle.min.js" hash="a5cf86fa138eccdd8aa3d39b170a1687"/><file name="algoliaBundle.min.js.map" hash="95baad19b0b4f8de10eee092ef060174"/></dir></dir><dir name="lib"><dir name="AlgoliaSearch"><file name="AlgoliaException.php" hash="ad654ffdc97c62dc798983cacddc17fd"/><file name="Client.php" hash="4e8312c04d8e13d9fed49416b20d46b8"/><file name="ClientContext.php" hash="8d23dddb0b737db86505c254ae0fbc22"/><file name="Index.php" hash="182e30382a5484a297732f88d4355e59"/><file name="IndexBrowser.php" hash="6e9b065d97a5ac67a120a451024c7c17"/><file name="PlacesIndex.php" hash="d5cbb714b558097344cbbad9f2cc1bb6"/><file name="Version.php" hash="dbfcd97d08bf3043f183fc8cee18d8e6"/><dir name="resources"><file name="ca-bundle.crt" hash="47961e7ef15667c93cd99be01b51f00a"/></dir></dir></dir></target></contents>
28
  <compatible/>
29
  <dependencies><required><php><min>5.2.0</min><max>6.0.0</max></php></required></dependencies>
30
  </package>