├── .github └── ISSUE_TEMPLATE.md ├── README.md ├── api-client-specifications ├── README.md ├── analytics_api.md ├── insights_api.md ├── personalization_api.md ├── recommend_api.md └── search_api.md ├── common-test-suite └── README.md └── internals ├── README.md ├── retry_strategy.md ├── search_client_get_secured_api_key_remaining_validity.md └── search_index_exists.md /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Summary** 2 | 3 | 8 | 9 | **Links** 10 | 11 | 17 | - [TITLE](URL) 18 | 19 | **API Clients issues** 20 | 21 | 27 | 28 | - [ ] C# 29 | - [ ] Go 30 | - [ ] Java 31 | - [ ] JavaScript 32 | - [ ] Kotlin 33 | - [ ] PHP 34 | - [ ] Python 35 | - [ ] Ruby 36 | - [ ] Scala 37 | - [ ] Swift 38 | 39 | **Documentation snippets** 40 | 41 | 47 | 48 | - [Documentation snippets PR]() 49 | - [ ] C# 50 | - [ ] Go 51 | - [ ] Java 52 | - [ ] JavaScript 53 | - [ ] Kotlin 54 | - [ ] PHP 55 | - [ ] Python 56 | - [ ] Ruby 57 | - [ ] Scala 58 | - [ ] Swift 59 | 60 | **Releases** 61 | 62 | 68 | 69 | - [ ] C# 70 | - [ ] Go 71 | - [ ] Java 72 | - [ ] JavaScript 73 | - [ ] Kotlin 74 | - [ ] PHP 75 | - [ ] Python 76 | - [ ] Ruby 77 | - [ ] Scala 78 | - [ ] Swift 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This repository is deprecated in favor of https://github.com/algolia/api-clients-automation. 3 | > You can still find the spec for the legacy clients in this repository, but they will not be updated. 4 | 5 |

6 | 7 | Algolia banner 8 | 9 | 10 |

The perfect starting point to implement a new Algolia API client in your language of choice

11 |

12 | 13 |

14 | Documentation • 15 | Community Forum • 16 | Stack Overflow • 17 | Report a bug • 18 | Support 19 |

20 | 21 | ## ✨ Summary 22 | 23 | This repository serves as a technical documentation of the public API of the 24 | Algolia API Clients. It covers both the public API (methods, functions, etc.), 25 | their test coverage and implementation details. 26 | 27 | The goal of this repository is to expose a common and consistent implementation 28 | across all our official API clients: 29 | 30 | - [algolia/algoliasearch-client-csharp](https://github.com/algolia/algoliasearch-client-csharp) 31 | - [algolia/algoliasearch-client-go](https://github.com/algolia/algoliasearch-client-go) 32 | - [algolia/algoliasearch-client-java-2 (compatible with Java 1.8 and above)](https://github.com/algolia/algoliasearch-client-java-2) 33 | - [algolia/algoliasearch-client-java (compatible with Java 1.7 and below)](https://github.com/algolia/algoliasearch-client-java) 34 | - [algolia/algoliasearch-client-javascript](https://github.com/algolia/algoliasearch-client-javascript) 35 | - [algolia/algoliasearch-client-kotlin](https://github.com/algolia/algoliasearch-client-kotlin) 36 | - [algolia/algoliasearch-client-php](https://github.com/algolia/algoliasearch-client-php) 37 | - [algolia/algoliasearch-client-python](https://github.com/algolia/algoliasearch-client-python) 38 | - [algolia/algoliasearch-client-ruby](https://github.com/algolia/algoliasearch-client-ruby) 39 | - [algolia/algoliasearch-client-scala](https://github.com/algolia/algoliasearch-client-scala) 40 | - [algolia/algoliasearch-client-swift](https://github.com/algolia/algoliasearch-client-swift) 41 | 42 | ## 📦 What's included 43 | 44 | - 📜 [Public API Specifications (Specs)](api-client-specifications/) 45 | - ✅ [Common Test Suite (CTS)](common-test-suite/) 46 | - 🛠 [Implementation Details (Internals)](internals/) 47 | -------------------------------------------------------------------------------- /api-client-specifications/README.md: -------------------------------------------------------------------------------- 1 | # API Client Specifications 2 | 3 | This repository contains the public API specifications of the official Algolia 4 | API clients as well as some details regarding some internal implementations. 5 | 6 | ## Table Of Contents 7 | 8 | - [Search API](search_api.md) 9 | - [Analytics API](analytics_api.md) 10 | - [Insights API](insights_api.md) 11 | - [Personalization API](personalization_api.md) 12 | - [Recommend API](recommend_api.md) 13 | -------------------------------------------------------------------------------- /api-client-specifications/analytics_api.md: -------------------------------------------------------------------------------- 1 | # Analytics API clients specifications 2 | 3 | ## Table of Contents 4 | 5 | - [`analytics_client` interface](#analytics_client-interface) 6 | - [Objects](#objects) 7 | - [Responses](#responses) 8 | 9 | ## `analytics_client` interface 10 | 11 | ```java 12 | function init_analytics_client(appid: string, api_key: string) returns analytics_client 13 | function init_analytics_client_with_config(config: analytics_configuration) returns analytics_client 14 | 15 | interface analytics_client { 16 | 17 | function add_ab_test(ab: ab_test, opts: request_options) return task_ab_test_response 18 | function get_ab_test(id: int, opts: request_options) return get_ab_test_response 19 | function get_ab_tests(opts: request_options) return get_ab_tests_response 20 | function stop_ab_test(id: int, opts: request_options) return task_ab_test_response 21 | function delete_ab_test(id: int, opts: request_options) return task_ab_test_response 22 | 23 | } 24 | ``` 25 | 26 | ## Objects 27 | 28 | ```ts 29 | struct ab_test { 30 | name: string 31 | variants: [variant] 32 | endAt: string // Format ISO8601 "2006-01-02T15:04:05Z" 33 | } 34 | 35 | struct variant { 36 | index: string 37 | trafficPercentage: int 38 | description: string (optional) 39 | customSearchParameters: map (optional) // Accepts any search_parameter 40 | } 41 | ``` 42 | 43 | ## Responses 44 | 45 | ```ts 46 | struct task_ab_test_response { 47 | abTestID: int 48 | index: string 49 | taskID: int 50 | } 51 | 52 | struct get_ab_tests_response { 53 | abtests: [ab_test_response] 54 | count: int 55 | total: int 56 | } 57 | 58 | struct ab_test_response { 59 | abTestID: int 60 | clickSignificance: int 61 | conversionSignificance: float 62 | createdAt: string 63 | endAt: string 64 | name: string 65 | status: string 66 | variants: [variant_response] 67 | } 68 | 69 | struct variant_response { 70 | averageClickPosition: int 71 | clickCount: int 72 | clickThroughRate: float 73 | conversionCount: int 74 | conversionRate: float 75 | description: string 76 | index: string 77 | noResultCount: int 78 | searchCount: int 79 | trackedSearchCount: int 80 | trafficPercentage: int 81 | userCount: int 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /api-client-specifications/insights_api.md: -------------------------------------------------------------------------------- 1 | # Insights API clients specifications 2 | 3 | ## Table of Contents 4 | 5 | - [`insights_client` interface](#insights_client-interface) 6 | - [`insights_user_client` interface](#insights_user_client-interface) 7 | - [Objects](#objects) 8 | - [Responses](#responses) 9 | 10 | ## `insights_client` interface 11 | 12 | ```java 13 | interface insights_client { 14 | function user(userToken: string) return insights_user_client 15 | function send_event(event: event, opts: request_options) return status_message_response 16 | function send_events(events: [event], opts: request_options) return status_message_response 17 | } 18 | ``` 19 | 20 | ## `insights_user_client` interface 21 | 22 | ```java 23 | interface insights_user_client { 24 | function clicked_object_ids(eventName: string, indexName: string, objectIDs: [string], opts: request_options) return status_message_response 25 | function clicked_object_ids_after_search(eventName: string, indexName: string, objectIDs: [string], positions: [int], queryID: string, opts: request_options) return status_message_response 26 | function clicked_filters(eventName: string, indexName: string, filters: [string], opts: request_options) return status_message_response 27 | 28 | function converted_object_ids(eventName: string, indexName: string, objectIDs: [string], opts: request_options) return status_message_response 29 | function converted_object_ids_after_search(eventName: string, indexName: string, objectIDs: [string], queryID: string, opts: request_options) return status_message_response 30 | function converted_filters(eventName: string, indexName: string, filters: [string], opts: request_options) return status_message_response 31 | 32 | function viewed_object_ids(eventName: string, indexName: string, objectIDs: [string], opts: request_options) return status_message_response 33 | function viewed_filters(eventName: string, indexName: string, filters: [string], opts: request_options) return status_message_response 34 | } 35 | ``` 36 | 37 | ## Objects 38 | 39 | ```java 40 | struct event { 41 | eventType: string 42 | eventName: string 43 | index: string 44 | userToken: string 45 | timestamp: int // Unix epoch timestamp (in second) 46 | objectIDs: [string] 47 | positions: [int] 48 | queryID: string 49 | filters: [string] 50 | } 51 | ``` 52 | 53 | ## Responses 54 | 55 | ```java 56 | struct status_message_response { 57 | status: int 58 | message: string 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /api-client-specifications/personalization_api.md: -------------------------------------------------------------------------------- 1 | # Personalization API clients specifications 2 | 3 | ## Table of Contents 4 | 5 | - [`personalization_client` interface](#personalization_client-interface) 6 | - [Objects](#objects) 7 | - [Responses](#responses) 8 | 9 | ## `personalization_client` interface 10 | 11 | ```java 12 | interface personalization_client { 13 | function get_personalization_profile(userToken: string, opts: request_options) 14 | return get_personalization_profile_response 15 | 16 | function delete_personalization_profile(userToken: string, opts: request_options) 17 | return delete_personalization_profile_response 18 | 19 | function get_personalization_strategy(opts: request_options) 20 | return get_personalization_strategy_response 21 | 22 | function set_personalization_strategy(set_strategy_request: strategy, opts: request_options) 23 | return set_personalization_strategy_response 24 | } 25 | ``` 26 | 27 | ## Objects 28 | 29 | ```java 30 | struct set_strategy_request { 31 | eventsScoring: events_scoring[] 32 | facetsScroing: facets_scoring[] 33 | personalizationImpact: int 34 | } 35 | ``` 36 | 37 | ```java 38 | struct events_scoring { 39 | eventName: string 40 | eventType: string 41 | score: int 42 | } 43 | ``` 44 | 45 | ```java 46 | struct facets_scoring { 47 | facetName: string 48 | score: int 49 | } 50 | ``` 51 | 52 | ## Responses 53 | 54 | ```java 55 | struct get_personalization_profile_response { 56 | userToken: string, 57 | lastEventAt: datetime, 58 | scores: object 59 | } 60 | ``` 61 | 62 | ```java 63 | struct delete_personalization_profile_response { 64 | userToken: string, 65 | deletedUntil: datetime 66 | } 67 | ``` 68 | 69 | ```java 70 | struct get_personalization_strategy_response { 71 | eventsScoring: events_scoring[] 72 | facetsScroing: facets_scoring[] 73 | personalizationImpact: int 74 | } 75 | ``` 76 | 77 | ```java 78 | struct set_personalization_strategy_response { 79 | status: int 80 | message: string 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /api-client-specifications/recommend_api.md: -------------------------------------------------------------------------------- 1 | # Recommend API clients specifications 2 | 3 | ## Table of Contents 4 | 5 | - [`recommend_client` interface](#recommend_client-interface) 6 | - [Objects](#objects) 7 | - [Responses](#responses) 8 | 9 | ## `recommend_client` interface 10 | 11 | ```java 12 | interface recommend_client { 13 | function get_recommendations(queries: [recommendations_query]) return get_recommendations_response 14 | function get_related_products(queries: [related_products_query]) return get_recommendations_response 15 | function get_frequently_bought_together(queries: [frequently_bought_together_query]) return get_recommendations_response 16 | } 17 | ``` 18 | 19 | ## Objects 20 | 21 | ```java 22 | struct recommendations_search_options { 23 | // all algolia search parameters except the pagination: page, hitsPerPage, offset, length 24 | // https://www.algolia.com/doc/api-reference/search-api-parameters/ 25 | } 26 | ``` 27 | 28 | ```java 29 | struct recommendations_query { 30 | indexName: string, 31 | model: 'bought-together' | 'related-products', 32 | objectID: string, 33 | // optional 34 | threshold: float, // default 0, between 0 and 100 35 | maxRecommendations: int, 36 | queryParameters: recommendations_search_options, 37 | fallbackParameters: recommendations_search_options 38 | } 39 | ``` 40 | 41 | ```java 42 | struct related_products_query { 43 | indexName: string, 44 | objectID: string, 45 | // optional 46 | threshold: float, // default 0, between 0 and 100 47 | maxRecommendations: int, 48 | queryParameters: recommendations_search_options, 49 | fallbackParameters: recommendations_search_options 50 | } 51 | ``` 52 | 53 | ```java 54 | struct frequently_bought_together_query { 55 | indexName: string, 56 | objectID: string, 57 | // optional 58 | threshold: float, // default 0, between 0 and 100 59 | maxRecommendations: int, 60 | queryParameters: recommendations_search_options, 61 | } 62 | ``` 63 | 64 | ## Responses 65 | 66 | ```java 67 | struct get_recommendations_reponse_hit { 68 | _score: float, // https://www.algolia.com/doc/api-reference/api-methods/get-recommendations/#method-response-_score 69 | // + all search query response hit: https://www.algolia.com/doc/api-reference/api-methods/search/#method-response-hits 70 | } 71 | ``` 72 | 73 | ```java 74 | struct get_recommendations_result { 75 | hits: [get_recommendations_reponse_hit] 76 | // + all search query response options: https://www.algolia.com/doc/api-reference/api-methods/search/#response 77 | } 78 | ``` 79 | 80 | ```java 81 | struct get_recommendations_reponse { 82 | results: [get_recommendations_result] 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /api-client-specifications/search_api.md: -------------------------------------------------------------------------------- 1 | # Search API clients specifications 2 | 3 | ## Table of Contents 4 | 5 | - [`account_client` interface](#account_client-interface) 6 | - [`search_client` interface](#search_client-interface) 7 | - [`search_index` interface](#search_index-interface) 8 | - [Objects](#objects) 9 | - [Responses](#responses) 10 | 11 | ## `account_client` interface 12 | 13 | - Cross-application **and** cross-index operations 14 | 15 | ```java 16 | interface account_client { 17 | 18 | function copy_index(source: search_index, destination: search_index) returns waitable 19 | 20 | } 21 | ``` 22 | 23 | ## `search_client` interface 24 | 25 | - Inter-application **but** cross-index operations 26 | 27 | ```java 28 | interface search_client { 29 | 30 | // Misc 31 | function init_index(index_name: string) return search_index 32 | function list_indexes(opts: request_options) return list_indexes_response 33 | function get_logs(offset: int = 0, length: int = 10, type: log_type) return get_logs_response 34 | 35 | // Copy index operations 36 | function copy_rules(source: string, destination: string, opts: request_options) return task_updated_response 37 | function copy_settings(source: string, destination: string, opts: request_options) return task_updated_response 38 | function copy_synonyms(source: string, destination: string, opts: request_options) return task_updated_response 39 | function copy_index(source: string, destination: string, scopes: [scope], opts: request_options) return task_updated_response 40 | 41 | // API key methods 42 | function get_api_key(keyID: string, opts: request_options) return key 43 | function add_api_key(key: key, opts: request_options) return key_created_response 44 | function update_api_key(key: key, opts: request_options) return key_updated_response 45 | function delete_api_key(key: key, opts: request_options) return deleted_response 46 | function restore_api_key(keyID: string, opts: request_options) return created_response 47 | function list_api_keys(opts: request_options) return list_api_keys_response 48 | function generate_secured_api_key(parentKey: string, restriction: secured_api_key_restriction) return secured_api_key 49 | function get_secured_api_key_remaining_validity(secured_api_key: string) return int // or a duration, see `internals/` 50 | 51 | // Multiple* methods 52 | function multiple_batch(operations: [indexed_operation], opts: request_options) return multiple_batch_response 53 | function multiple_get_objects(requests: [indexed_get_object], opts: request_options) return [] 54 | function multiple_queries(queries: [indexed_query], opts: request_options) return multiple_queries_response 55 | 56 | // Multi-Cluster Management (MCM) methods 57 | function assign_user_id(userID: string, clusterName: string, opts: request_options) return created_response 58 | function assign_user_ids(userIDs: []string, clusterName: string, opts: request_options) return created_response 59 | function get_top_user_id(opts: request_options) return get_top_user_id_response 60 | function get_user_id(userID: string, opts: request_options) return get_user_id_response 61 | function list_clusters(opts: request_options) return list_clusters_response 62 | function list_user_ids(page: int = 0, hitsPerPage: int = 20, opts: request_options) return list_user_ids_response 63 | function remove_user_id(userID: string, opts: request_options) return deleted_response 64 | function search_user_ids(query: string, clusterName: string = null, page: int = 0, hitsPerPage: int = 20, opts: request_options) return search_user_ids_response 65 | function has_pending_mappings(retrieveMappings: boolean = false, opts: request_options) return has_pending_mappings_response 66 | } 67 | ``` 68 | 69 | ## `search_index` interface 70 | 71 | - Inter-application **and** inter-index operations 72 | 73 | ```ts 74 | interface search_index { 75 | 76 | // Misc 77 | function wait_task(taskID: int) 78 | function get_status(taskID: int) return task_status_response 79 | function get_app_id() return string 80 | function clear(opts: request_options) return task_updated_response 81 | function delete(opts: request_options) return task_deleted_response 82 | function delete_replica(replica_name: string, opts: request_options) 83 | 84 | // Indexing 85 | function get_object(objectID: string, opts: request_options) return 86 | function get_objects(objectIDs: [string], opts: request_options) return [] 87 | function save_object(object: , opts: request_options) return task_created_object_id_response 88 | function save_objects(objects: [], opts: request_options) return group_batch_response 89 | function partial_update_object(object: , opts: request_options) return task_updated_response 90 | function partial_update_objects(objects: [], opts: request_options) return group_batch_response 91 | function delete_object(objectID: string, opts: request_options) return task_deleted_response 92 | function delete_objects(objectIDs: [string], opts: request_options) return batch_response 93 | function delete_by(opts: request_options) return task_updated_response 94 | function batch(operations: [batch_operation], opts: request_options) return batch_response 95 | 96 | // Query rules 97 | function get_rule(objectID: string, opts: request_options) return rule 98 | function save_rule(rule: rule, forward_to_replicas: bool = false, opts: request_options) return task_updated_response 99 | function save_rules(rule: [rule], forward_to_replicas: bool = false, clear_existing_rules: bool = false, opts: request_options) return task_updated_response 100 | function clear_rules(forward_to_replicas: bool = false, opts: request_options) return task_updated_response 101 | function delete_rule(objectID: string, forward_to_replicas: bool = false, opts: request_options) return task_updated_response 102 | 103 | // Synonyms 104 | function get_synonym(objectID: string, opts: request_options) return synonym 105 | function save_synonym(synonym: synonym, forward_to_replicas: bool = false, opts: request_options) return task_updated_response 106 | function save_synonyms(synonym: [synonym], forward_to_replicas: bool = false, replace_existing_synonyms: bool = false, opts: request_options) return task_updated_response 107 | function clear_synonyms(forward_to_replicas: bool = false, opts: request_options) return task_updated_response 108 | function delete_synonym(objectID: string, forward_to_replicas: bool = false, opts: request_options) return task_deleted_response 109 | 110 | // Browsing 111 | function browse(query string, params: [browse_parameter], opts: request_options) return browse_response 112 | function browse_objects(params: [query_browse_parameter], opts: request_options) return object_iterator 113 | function browse_rules(opts: request_options) return rule_iterator 114 | function browse_synonyms(opts: request_options) return synonym_iterator 115 | 116 | // Replacing 117 | function replace_all_objects(objects: , opts: request_options) 118 | function replace_all_rules(rules: [rule], forward_to_replicas: bool = false, opts: request_options) return task_updated_response 119 | function replace_all_synonyms(synonyms: [synonym], forward_to_replicas: bool = false, opts: request_options) return task_updated_response 120 | 121 | // Searching 122 | function search(query string, params: [search_parameter], opts: request_options) return query_response 123 | function search_for_facet_values(facet_name: string, facet_query: string, params: [search_parameter], opts: request_options) return query_response 124 | function search_rules(query string, params: [rule_search_parameter], opts: request_options) return rule_query_response 125 | function search_synonyms(query string, params: [synonym_search_parameter], opts: request_options) return synonym_query_response 126 | 127 | // Settings 128 | function get_settings(opts: request_options) return settings 129 | function set_settings(settings: settings, opts: request_options) return task_updated_response 130 | 131 | // Exists 132 | function exists() return bool 133 | 134 | // find_object search iteratively through the search response `hits` 135 | // field to find the first response hit that would match against the given 136 | // `filter_func` function. The name of the argument `filter_func` may change 137 | // up the language, as example you may use `callback` in php. 138 | // 139 | // If no object has been found within the first result set, the function 140 | // will perform a new search operation on the next page of results, if any, 141 | // until a matching object is found or the end of results, whichever 142 | // happens first. 143 | // 144 | // To prevent the iteration through pages of results, `paginate` in 145 | // request_options can be set to false. This will stop the function at the end of 146 | // the first page of search results even if no object does match. 147 | 148 | // Of course, the `opts` parameter, should be used behind the scenes by the 149 | // search method. And, in same languages, the `opts` parameter may contain all 150 | // the optional parameters. 151 | function find_object( 152 | filter_func: function (object: T) return bool, // function matching a specific record 153 | query: string = '', // an optional the search query 154 | paginate: bool = true, // an optional boolean to prevent pagination 155 | opts: request_options // may contain the argument `query`, `pagination` or any search parameter 156 | ) return object_with_position 157 | } 158 | ``` 159 | 160 | ## Objects 161 | 162 | ```ts 163 | enum log_type { "all", "query", "build", "error" } 164 | 165 | enum scope { "rules", "settings", "synonyms" } 166 | 167 | struct request_options { 168 | extra_headers: map 169 | extra_url_params: map 170 | } 171 | 172 | struct browse_parameter // https://www.algolia.com/doc/api-reference/api-methods/browse/#method-param-browseparameters 173 | struct search_parameter // https://www.algolia.com/doc/api-reference/api-methods/search/#method-param-searchparameters 174 | struct rule_search_parameter // https://www.algolia.com/doc/api-reference/api-methods/search-rules/#parameters 175 | struct synonym_search_parameter // https://www.algolia.com/doc/api-reference/api-methods/search-synonyms/#parameters 176 | 177 | struct query_browse_parameter { 178 | query: string 179 | // + all browse_parameter fields 180 | } 181 | 182 | struct key // https://www.algolia.com/doc/api-reference/api-methods/add-api-key/#parameters 183 | 184 | struct indexed_operation { 185 | indexName: string 186 | // + all batch_operation fields 187 | } 188 | 189 | struct indexed_get_object { 190 | indexName: string 191 | objectID: string 192 | attributesToRetrieve: string // Comma-separated string 193 | } 194 | 195 | struct indexed_query { 196 | indexName: string 197 | // + all query fields 198 | } 199 | 200 | struct object_iterator // Language-specific representation of an iterator on arbitrary objects 201 | struct rule_iterator // Language-specific representation of an iterator on rules 202 | struct synonym_iterator // Language-specific representation of an iterator on synonyms 203 | 204 | struct rule // https://www.algolia.com/doc/api-reference/api-methods/save-rule/#method-param-rule 205 | struct settings // https://www.algolia.com/doc/api-reference/settings-api-parameters/ 206 | struct synonym // https://www.algolia.com/doc/api-reference/api-methods/save-synonym/#method-param-synonym-object 207 | 208 | struct strategy { 209 | eventsScoring: map 210 | facetsScoring: map 211 | } 212 | 213 | struct events_scoring { 214 | score: int 215 | type: string 216 | } 217 | 218 | struct facets_scoring { 219 | score: int 220 | } 221 | 222 | struct secured_api_key_restriction // https://www.algolia.com/doc/api-reference/api-methods/generate-secured-api-key/#parameters 223 | 224 | struct object_with_position { 225 | object: T 226 | position: int 227 | page: int 228 | } 229 | ``` 230 | 231 | ## Responses 232 | 233 | All response objects are implemented with the `waitable` interface in order for the 234 | user to be able to wait their completion without dealing with the low-level 235 | details. 236 | 237 | ```ts 238 | interface waitable { 239 | 240 | function wait() return error 241 | 242 | } 243 | ``` 244 | 245 | ```ts 246 | struct query_response { // https://www.algolia.com/doc/api-reference/api-methods/search/#response 247 | // get_object_position returns the position (0-based) within the `hits` 248 | // result list of the record matching against the given objectID. If the 249 | // objectID is not found, -1 is returned. 250 | function get_object_position(objectID: string) return int 251 | } 252 | 253 | struct rule_query_response // https://www.algolia.com/doc/api-reference/api-methods/search-rules/#response 254 | 255 | struct group_batch_response { 256 | responses: [batch_response] 257 | } 258 | 259 | struct batch_response { 260 | objectIDs: [string] 261 | taskID: int 262 | } 263 | 264 | struct multiple_batch_response { 265 | objectIDs: [string] 266 | taskID: map // Mapping of (index name -> taskID) 267 | } 268 | 269 | struct multiple_queries_response { 270 | results: [multiple_queries_query_response] 271 | } 272 | 273 | struct multiple_queries_query_response { 274 | processed: bool 275 | // + all query_response fields 276 | } 277 | 278 | struct get_top_user_id_response // https://www.algolia.com/doc/api-reference/api-methods/get-top-user-id/#response 279 | struct get_user_id_response // https://www.algolia.com/doc/api-reference/api-methods/get-user-id/#response 280 | struct list_clusters_response // https://www.algolia.com/doc/api-reference/api-methods/list-clusters/#response 281 | struct list_user_ids_response // https://www.algolia.com/doc/api-reference/api-methods/list-user-id/#response 282 | struct search_user_ids_response // https://www.algolia.com/doc/api-reference/api-methods/search-user-id/#response 283 | struct has_pending_mappings_response { 284 | pending: boolean, 285 | clusters: map 286 | } 287 | 288 | struct list_indexes_response // https://www.algolia.com/doc/api-reference/api-methods/list-indices/#response 289 | struct get_logs_response // https://www.algolia.com/doc/api-reference/api-methods/get-logs/#response 290 | 291 | struct task_updated_response { 292 | taskID: int 293 | updatedAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 294 | } 295 | 296 | struct task_deleted_response { 297 | taskID: int 298 | deletedAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 299 | } 300 | 301 | struct task_status_response { 302 | status: string 303 | pendingTask: bool 304 | } 305 | 306 | struct task_created_object_id_response { 307 | createdAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 308 | objectID: string 309 | taskID: string 310 | } 311 | 312 | struct created_response { 313 | createdAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 314 | } 315 | 316 | struct deleted_response { 317 | deletedAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 318 | } 319 | 320 | struct key_created_response { 321 | key: string 322 | createdAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 323 | } 324 | 325 | struct key_updated_response { 326 | key: string 327 | updatedAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 328 | } 329 | 330 | struct list_api_keys_response // https://www.algolia.com/doc/api-reference/api-methods/list-api-keys/#response 331 | 332 | struct browse_response // https://www.algolia.com/doc/api-reference/api-methods/browse/#response 333 | 334 | struct set_personalization_strategy_response { 335 | updatedAt: string // Format RFC3339 "2006-01-02T15:04:05Z07:00" 336 | } 337 | 338 | struct get_personalization_strategy_response { 339 | taskID: int 340 | // + all fields fields 341 | } 342 | ``` 343 | -------------------------------------------------------------------------------- /common-test-suite/README.md: -------------------------------------------------------------------------------- 1 | # Common Test Suite 2 | 3 | ## Table of Contents 4 | 5 | * [Rationale](#rationale) 6 | * [API clients](#api-clients) 7 | * [Test suite](#test-suite) 8 | * [Environment variables](#environment-variables) 9 | * [Index naming convention](#index-naming-convention) 10 | * [Tests (index)](#tests-index) 11 | * [Indexing](#indexing) 12 | * [Settings](#settings) 13 | * [Search](#search) 14 | * [Synonyms](#synonyms) 15 | * [Query rules](#query-rules) 16 | * [Batching](#batching) 17 | * [Replacing](#Replacing) 18 | * [Exists](#Exists) 19 | * [Tests (client)](#tests-client) 20 | * [Copy index](#copy-index) 21 | * [Multi Cluster Management (MCM)](#multi-cluster-management-mcm) 22 | * [API keys](#api-keys) 23 | * [Get logs](#get-logs) 24 | * [Multiple Operations](#multiple-operations) 25 | * [DNS timeout](#dns-timeout) 26 | * [Tests (account)](#tests-account) 27 | * [Copy index](#copy-index-1) 28 | * [Tests (secured API keys)](#tests-secured-api-keys) 29 | * [Generate secured API keys](#generate-secured-api-keys) 30 | * [Expired Secured API keys](#expired-secured-api-keys) 31 | * [Tests (analytics)](#tests-analytics) 32 | * [AB testing](#ab-testing) 33 | * [AA testing](#aa-testing) 34 | * [Tests (insights)](#tests-insights) 35 | * [Sending events](#sending-events) 36 | * [Tests (personalization)](#tests-personalization) 37 | * [Personalization Strategy](#personalization-strategy) 38 | * [Tests (backward compatibility)](#tests-backward-compatibility) 39 | * [Old settings](#old-settings) 40 | * [Query rules v1](#query-rules-v1) 41 | 42 | ## Rationale 43 | 44 | This document intends to specify all tests that must be implemented for all 45 | Algolia API clients. Note that API clients may implement more tests, depending 46 | on implementations. The list is to be considered as a required minimum. 47 | 48 | ## API clients 49 | 50 | * C# 51 | * Go 52 | * Java 53 | * PHP 54 | * Python 55 | * Ruby 56 | * Scala 57 | 58 | ## Test suite 59 | 60 | ### Environment variables 61 | 62 | To ease the testing process, all configuration must be passed via environment 63 | variables. To run the tests, the following environment variables must be set: 64 | 65 | | Name | Value | 66 | | ---------------------------- | ------------------------------------------------- | 67 | | `ALGOLIA_APPLICATION_ID_1` | `NOCTT5TZUU` | 68 | | `ALGOLIA_ADMIN_KEY_1` | (Personify `NOCTT5TZUU`, then get the admin key) | 69 | | `ALGOLIA_SEARCH_KEY_1` | (Personify `NOCTT5TZUU`, then get the search key) | 70 | | `ALGOLIA_APPLICATION_ID_2` | `UCX3XB3SH4` | 71 | | `ALGOLIA_ADMIN_KEY_2` | (Personify `UCX3XB3SH4`, then get the admin key) | 72 | | `ALGOLIA_APPLICATION_ID_MCM` | `P0OGQ40IKV` | 73 | | `ALGOLIA_ADMIN_KEY_MCM` | (Personify `P0OGQ40IKV`, then get the admin key) | 74 | 75 | 76 | Application `NOCTT5TZUU` must be used for all the tests except the MCM tests 77 | that needs to be done using application `P0OGQ40IKV` for which MCM has been 78 | enabled. The second regular application `UCX3XB3SH4` is used to test the 79 | `AccountClient` features which are performing cross-application operations. 80 | 81 | ### Index naming convention 82 | 83 | For each of the following test, one or more index may be used. In order to 84 | avoid tests to compromise each other when running in parallel, the following 85 | naming convention should be used: 86 | 87 | `LANG_DATE_TIME_INSTANCE_TEST` 88 | 89 | Where: 90 | * `LANG` corresponds to the API client language or integration name (go, php, 91 | java, ruby, rails, etc.) which is unique for each client/integration. 92 | * `DATE` corresponds to the current time according to the following format: 93 | * `YYYY-MM-DD` 94 | * `TIME` corresponds to the current time according to the following format: 95 | * `HH:mm:ss` 96 | * `INSTANCE` corresponds to 97 | * the `TRAVIS_JOB_NUMBER` environment variable if available, otherwise 98 | * the current username on the system running the test if available, otherwise "unknown" 99 | * `TEST` is the name of the current test 100 | 101 | Note that you need to make sure your code is using UTC timezone. 102 | 103 | Let’s take some examples: if I, Anthony, was run the "indexing" test on my 104 | machine, as of today (September, 11th 2018, 6:05:39 PM) for the Go client, the 105 | name of the index that would be created should be: 106 | 107 | `go_2018-09-11_18:05:39_anthony_indexing` 108 | 109 | This naming enables us to run tests in parallel, across different environments 110 | (local computer, CI pipelines, etc.) without impacting other tests. 111 | 112 | ### Tests (index) 113 | 114 | #### Indexing 115 | 116 | * Instantiate the client and index `indexing` 117 | * Add 1 record with **saveObject** with an objectID and collect taskID/objectID 118 | * Add 1 record with **saveObject** without an objectID and collect taskID/objectID 119 | * Perform a **saveObjects** with an empty set of objects and collect taskID 120 | * Add 2 records with **saveObjects** with an objectID and collect taskID/objectID 121 | * Add 2 records with **saveObjects** without an objectID and collect taskID/objectID 122 | * Sequentially send 10 batches of 100 objects with objectID from 1 to 1000 with **batch** and collect taskIDs/objectIDs 123 | * Wait for all collected tasks to terminate with **waitTask** 124 | * Retrieve the 6 first records with **getObject** and check their content against original records 125 | * Retrieve the 1000 remaining records with **getObjects** with objectIDs from 1 to 1000 and check their content against original records 126 | * Browse all records with **browseObjects** and make sure we have browsed 1006 records, and check that all objectIDs are found 127 | * Alter 1 record with **partialUpdateObject** and collect taskID/objectID 128 | * Alter 2 records with **partialUpdateObjects** and collect taskID/objectID 129 | * Wait for all collected tasks to terminate with **waitTask** 130 | * Retrieve all the previously altered records with **getObject** and check their content against the modified records 131 | * Add 1 record with **saveObject** with an objectID and a tag `algolia` and wait for the task to finish 132 | * Delete the first record with **deleteObject** and collect taskID 133 | * Delete the record containing the tag `algolia` with **deleteBy** and the `tagFilters` option and collect taskID 134 | * Delete the 5 remaining first records with **deleteObjects** and collect taskID 135 | * Delete the 1000 remaining records with **clearObjects** and collect taskID 136 | * Wait for all collected tasks to terminate 137 | * Browse all objects with **browseObjects** and make sure that no records are returned 138 | 139 | #### Settings 140 | 141 | * Instantiate the client and index `settings` and keep the generated index name (which will be used as a prefix of the replica indices) 142 | * Add one record to create the index with **saveObject** 143 | * Set the settings with the following parameters with **setSettings** and collect the taskID 144 | 145 | | Setting | Value | 146 | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | 147 | | `searchableAttributes` | `[ "attribute1", "attribute2", "attribute3", "ordered(attribute4)", "unordered(attribute5)" ]` | 148 | | `attributesForFaceting` | `[ "attribute1", "filterOnly(attribute2)", "searchable(attribute3)" ]` | 149 | | `unretrievableAttributes` | `[ "attribute1", "attribute2" ]` | 150 | | `attributesToRetrieve` | `[ "attribute3", "attribute4" ]` | 151 | | `ranking` | `[ "asc(attribute1)", "desc(attribute2)", "attribute", "custom", "exact", "filters", "geo", "proximity", "typo", "words" ]` | 152 | | `customRanking` | `[ "asc(attribute1)", "desc(attribute1)" ]` | 153 | | `replicas` | `[ indexname + "_replica1", indexname + "_replica2" ]` | 154 | | `maxValuesPerFacet` | `100` | 155 | | `sortFacetValuesBy` | `"count"` | 156 | | `attributesToHighlight` | `[ "attribute1", "attribute2" ]` | 157 | | `attributesToSnippet` | `[ "attribute1:10", "attribute2:8" ]` | 158 | | `highlightPreTag` | ``""`` | 159 | | `highlightPostTag` | ``""`` | 160 | | `snippetEllipsisText` | ``" and so on."`` | 161 | | `restrictHighlightAndSnippetArrays` | `true` | 162 | | `hitsPerPage` | `42` | 163 | | `paginationLimitedTo` | `43` | 164 | | `minWordSizefor1Typo` | `2` | 165 | | `minWordSizefor2Typos` | `6` | 166 | | `typoTolerance` | `false` | 167 | | `allowTyposOnNumericTokens` | `false` | 168 | | `ignorePlurals` | `true` | 169 | | `disableTypoToleranceOnAttributes` | `[ "attribute1", "attribute2" ]` | 170 | | `disableTypoToleranceOnWords` | `[ "word1", "word2" ]` | 171 | | `separatorsToIndex` | `"()[]"` | 172 | | `queryType` | `"prefixNone"` | 173 | | `removeWordsIfNoResults` | `"allOptional"` | 174 | | `advancedSyntax` | `true` | 175 | | `optionalWords` | `[ "word1", "word2" ]` | 176 | | `removeStopWords` | `true` | 177 | | `disablePrefixOnAttributes` | `[ "attribute1", "attribute2" ]` | 178 | | `disableExactOnAttributes` | `[ "attribute1", "attribute2" ]` | 179 | | `exactOnSingleWordQuery` | `"word"` | 180 | | `enableRules` | `false` | 181 | | `numericAttributesForFiltering` | `[ "attribute1", "attribute2" ]` | 182 | | `allowCompressionOfIntegerArray` | `true` | 183 | | `attributeForDistinct` | `"attribute1"` | 184 | | `distinct` | `2` | 185 | | `replaceSynonymsInHighlight` | `false` | 186 | | `minProximity` | `7` | 187 | | `responseFields` | `[ "hits", "hitsPerPage" ]` | 188 | | `maxFacetHits` | `100` | 189 | | `camelCaseAttributes` | `[ "attribute1", "attribute2" ]` | 190 | | `decompoundedAttributes` | `{ "de": ["attribute1", "attribute2"], "fi": ["attribute3"] }` | 191 | | `keepDiacriticsOnCharacters` | `"øé"` | 192 | | `queryLanguages` | `["en", "fr"]` | 193 | | `alternativesAsExact` | `["ignorePlurals"]` | 194 | | `advancedSyntaxFeatures` | `["exactPhrase"]` | 195 | | `userData` | `{"customUserData": 42.0}` | 196 | | `indexLanguages` | `["jp"]` | 197 | | `customNormalization` | `{"default": {"ä": "ae", "ö": "oe"}}}` 198 | | `enablePersonalization` | `true` | 199 | 200 | * Wait for the collected tasks to terminate with **waitTask** 201 | * Get the settings with **getSettings** and check that they correspond to the ones that were inserted 202 | * Set the settings with the following parameters with **setSettings** and collect the taskID 203 | 204 | | Setting | Value | 205 | | ----------------- | -------------- | 206 | | `typoTolerance` | `"min"` | 207 | | `ignorePlurals` | `["en", "fr"]` | 208 | | `removeStopWords` | `["en", "fr"]` | 209 | | `distinct` | `true` | 210 | 211 | * Wait for the collected tasks to terminate with **waitTask** 212 | * Get the settings with **getSettings** and check that they correspond to the ones that were inserted and overridden 213 | 214 | #### Search 215 | 216 | * Instantiate the client and index `search` 217 | * Add the following records with **saveObjects** and collect the taskID 218 | 219 | ``` 220 | [ 221 | {"company": "Algolia", "name": "Julien Lemoine", "objectID": "julien-lemoine"}, 222 | {"company": "Algolia", "name": "Nicolas Dessaigne", "objectID": "nicolas-dessaigne"}, 223 | {"company": "Amazon", "name": "Jeff Bezos"}, 224 | {"company": "Apple", "name": "Steve Jobs"}, 225 | {"company": "Apple", "name": "Steve Wozniak"}, 226 | {"company": "Arista Networks", "name": "Jayshree Ullal"}, 227 | {"company": "Google", "name": "Larry Page"}, 228 | {"company": "Google", "name": "Rob Pike"}, 229 | {"company": "Google", "name": "Serguey Brin"}, 230 | {"company": "Microsoft", "name": "Bill Gates"}, 231 | {"company": "SpaceX", "name": "Elon Musk"}, 232 | {"company": "Tesla", "name": "Elon Musk"}, 233 | {"company": "Yahoo", "name": "Marissa Mayer"} 234 | ] 235 | ``` 236 | 237 | * Set the following settings with **setSettings** and collect the taskID 238 | 239 | | Setting | Value | 240 | | ----------------------- | ------------------------- | 241 | | `attributesForFaceting` | `["searchable(company)"]` | 242 | 243 | * Wait for the collected tasks to terminate using **waitTask** 244 | * Perform a search query using **search** with the query `"algolia"` and no parameter and check that the number of returned hits is equal to 2 245 | * Call **get_object_id_position** with the objectID `"nicolas-dessaigne"` on the previous search response and check that it returns `0` 246 | * Call **get_object_id_position** with the objectID `"julien-lemoine"` on the previous search response and check that it returns `1` 247 | * Call **get_object_id_position** with the objectID `""` on the previous search response and check that it returns `-1` 248 | * Call **find_first_object** with the following parameters and check that no object is found 249 | 250 | | Parameter | Value | 251 | | ----------------- | -------------------------------------- | 252 | | `filter_func` | Function always returning `false` | 253 | | `query` | `""` | 254 | | `do_not_paginate` | `false` | 255 | | `params` | None | 256 | 257 | * Call **find_first_object** with the following parameters and check that the first object is returned with a `position=0` and `page=0` 258 | 259 | | Parameter | Value | 260 | | ----------------- | -------------------------------------- | 261 | | `filter_func` | Function always returning `true` | 262 | | `query` | `""` | 263 | | `do_not_paginate` | `false` | 264 | | `params` | None | 265 | 266 | * Call **find_first_object** with the following parameters and check that no object is found 267 | 268 | | Parameter | Value | 269 | | ----------------- | ------------------------------------------------------------------------ | 270 | | `filter_func` | Function returning `true` when an object with `company="Apple"` is found | 271 | | `query` | `"algolia"` | 272 | | `do_not_paginate` | `false` | 273 | | `params` | None | 274 | 275 | * Call **find_first_object** with the following parameters and check that no object is found 276 | 277 | | Parameter | Value | 278 | | ----------------- | ------------------------------------------------------------------------ | 279 | | `filter_func` | Function returning `true` when an object with `company="Apple"` is found | 280 | | `query` | `""` | 281 | | `do_not_paginate` | `true` | 282 | | `params` | `hitsPerPage=5` | 283 | 284 | * Call **find_first_object** with the following parameters and check that the first object is returned with a `position=0` and `page=2` 285 | 286 | | Parameter | Value | 287 | | ----------------- | ------------------------------------------------------------------------ | 288 | | `filter_func` | Function returning `true` when an object with `company="Apple"` is found | 289 | | `query` | `""` | 290 | | `do_not_paginate` | `false` | 291 | | `params` | `hitsPerPage=5` | 292 | 293 | * Perform a search using **search** with the query `"elon"` and the following parameter and check that the queryID field from the response is not empty 294 | 295 | | Parameter | Value | 296 | | ---------------- | ------ | 297 | | `clickAnalytics` | `true` | 298 | 299 | * Perform a faceted search using **search** with the query `"elon"` and the following parameters and check that the number of returned hits is equal to 1 300 | 301 | | Parameter | Value | 302 | | -------------- | ----------------- | 303 | | `facets"` | `"*"` | 304 | | `facetFilters` | `"company:tesla"` | 305 | 306 | * Perform a filtered search using **search** with the query `"elon"` and the following parameters and check that the number of returned hits is equal to 2 307 | 308 | | Parameter | Value | 309 | | --------- | ------------------------------------- | 310 | | `facets"` | `"*"` | 311 | | `filters` | `"(company:tesla OR company:spacex)"` | 312 | 313 | * Perform a facet search using **searchForFacetValue** with the facet `"company"` and the query `"a"` and no parameter and check that the facetHits field from the response contains the following values, in any order: 314 | * `"Algolia"` 315 | * `"Amazon"` 316 | * `"Apple"` 317 | * `"Arista Networks"` 318 | 319 | #### Synonyms 320 | 321 | * Instantiate the client and index `synonyms` 322 | * Add the following records using **saveObjects** and collect the taskID 323 | 324 | ``` 325 | [ 326 | {"console": "Sony PlayStation "}, 327 | {"console": "Nintendo Switch"}, 328 | {"console": "Nintendo Wii U"}, 329 | {"console": "Nintendo Game Boy Advance"}, 330 | {"console": "Microsoft Xbox"}, 331 | {"console": "Microsoft Xbox 360"}, 332 | {"console": "Microsoft Xbox One"} 333 | ] 334 | ``` 335 | 336 | * Add the following regular (n-way) synonym using **saveSynonym** and collect the taskID 337 | 338 | | ObjectID | Type | Synonym | 339 | | -------- | ----------------------------------------------------------------- | 340 | | `"gba"` | `"synonym"` | `[ "gba", "gameboy advance", "game boy advance" ]` | 341 | 342 | * Add the following synonyms using **saveSynonyms** and collect the taskID 343 | 344 | | ObjectID | Kind | Synonym | 345 | | ----------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | 346 | | `"wii_to_wii_u"` | One way synonym | `"wii"` → `["wii U"]` | 347 | | `"playstation_version_placeholder"` | Placeholder synonym | Placeholder: `""` Replacements: `[ "1", "One", "2", "3", "4", "4 Pro" ]` | 348 | | `"ps4"` | Alternative correction (1 typo) | `"ps4"` → `["playstation4"]` | 349 | | `"psone"` | Alternative correction (2 typos) | `"psone"` → `["playstationone"]` | 350 | 351 | * Wait for the collected tasks to terminate using **waitTask** 352 | * Retrieve the 5 added synonyms with **getSynonym** and check they are correctly retrieved 353 | * Perform a search query using **searchSynonyms** with the empty and check that the number of returned hits is equal to 5 354 | * Using **browseSynonyms** and iterate over all the synonyms and check that those collected synonyms are the same as the 5 originally saved 355 | * Delete the synonym with objectID `"gba"` using **deleteSynonym** and wait for the task to terminate using waitTask with the returned taskID 356 | * Try to get the synonym with **getSynonym** with objectID `"gba"` and check that the synonym does not exist anymore 357 | * Clear all the synonyms using **clearSynonyms** and wait for the task to terminate using waitTask with the returned taskID 358 | * Perform a synonym search using **searchSynonyms** with an empty query and check that the number of returned synonyms is equal to 0 359 | 360 | #### Query rules 361 | 362 | * Instantiate the client and index `rules` 363 | * Add the following records using **saveObjects** and collect the taskID 364 | 365 | ``` 366 | [ 367 | {"objectID": "iphone_7", "brand": "Apple", "model": "7"}, 368 | {"objectID": "iphone_8", "brand": "Apple", "model": "8"}, 369 | {"objectID": "iphone_x", "brand": "Apple", "model": "X"}, 370 | {"objectID": "one_plus_one", "brand": "OnePlus", "model": "One"}, 371 | {"objectID": "one_plus_two", "brand": "OnePlus", "model": "Two"}, 372 | ] 373 | ``` 374 | 375 | * Set **attributesForFaceting** to `["brand", "model"]` using **setSettings** and collect the taskID 376 | * Save the following rule using **saveRule** and collect the taskID 377 | 378 | ``` 379 | { 380 | "objectID": "brand_automatic_faceting", 381 | "enabled": false, 382 | "condition": {"anchoring": "is", "pattern": "{facet:brand}"}, 383 | "consequence": { 384 | "params": { 385 | "automaticFacetFilters": [ 386 | {"facet": "brand", "disjunctive": true, "score": 42}, 387 | ] 388 | } 389 | }, 390 | "validity": [ 391 | { 392 | "from": 1532439300, // 07/24/2018 13:35:00 UTC 393 | "until": 1532525700 // 07/25/2018 13:35:00 UTC 394 | }, 395 | { 396 | "from": 1532612100, // 07/26/2018 13:35:00 UTC 397 | "until": 1532698500 // 07/27/2018 13:35:00 UTC 398 | } 399 | ], 400 | "description": "Automatic apply the faceting on `brand` if a brand value is found in the query" 401 | } 402 | ``` 403 | 404 | * Save the following rules using **saveRules** and collect the taskID 405 | 406 | ``` 407 | [ 408 | { 409 | "objectID": "query_edits", 410 | "conditions": [{"anchoring": "is", "pattern": "mobile phone", "alternatives": true}], 411 | "consequence": { 412 | "filterPromotes": false, 413 | "params": { 414 | "query": { 415 | "edits": [ 416 | {"type": "remove", "delete": "mobile"}, 417 | {"type": "replace", "delete": "phone", "insert": "iphone"}, 418 | ] 419 | } 420 | } 421 | } 422 | }, 423 | { 424 | "objectID": "query_promo", 425 | "consequence": { 426 | "params": { 427 | "filters": "brand:OnePlus" 428 | } 429 | } 430 | }, 431 | { 432 | "objectID": "query_promo_summer", 433 | "condition": { 434 | "context": "summer" 435 | }, 436 | "consequence": { 437 | "params": { 438 | "filters": "model:One" 439 | } 440 | } 441 | } 442 | ] 443 | ``` 444 | 445 | * Wait for the previous tasks to complete using **waitTask** 446 | * Search with an empty query and the context `summer` and assert that the **`nbHits` is 1** 447 | * Retrieve all the rules using **getRule** and check that they were correctly saved 448 | * Retrieve all the rules using **searchRules** with {query: ""} and check that they were correctly saved 449 | * Iterate over all the rules using **ruleIterator** and check that they were correctly saved 450 | * Delete the first rule using **deleteRule** and check that it was correctly deleted using a **getRule**. 451 | * Clear all the remaining rules using **clearRules** and check that all rules have been correctly removed using **searchRules** with an empty query 452 | * As an extra test, to make sure we do support deserialization of Query Rule v1 format, try to deserialize the following JSON to the Query Rule structure or class (depending on the language) to ensure that pre-v2 query rules are still correctly deserialized and handled: 453 | 454 | ``` 455 | { 456 | "objectID": "query_edits", 457 | "condition": {"anchoring": "is", "pattern": "mobile phone"}, 458 | "consequence": { 459 | "params": { 460 | "query": { 461 | "remove": ["mobile", "phone"] 462 | } 463 | } 464 | } 465 | } 466 | ``` 467 | 468 | #### Batching 469 | 470 | * Instantiate the client and index `index_batching` 471 | * Add the following objects using **saveObjects** and wait for its completion using **waitTask** 472 | 473 | ``` 474 | [ 475 | {"objectID": "one", "key": "value"}, 476 | {"objectID": "two", "key": "value"}, 477 | {"objectID": "three", "key": "value"}, 478 | {"objectID": "four", "key": "value"}, 479 | {"objectID": "five", "key": "value"}, 480 | ] 481 | ``` 482 | 483 | * Send the following batch using **batch** and wait for its completion using **waitTask** 484 | 485 | ``` 486 | [ 487 | {"action": "addObject", "body": {"objectID": "zero", "key": "value"}}, 488 | {"action": "updateObject", "body": {"objectID": "one", "k": "v"}}, 489 | {"action": "partialUpdateObject", "body": {"objectID": "two", "k": "v"}}, 490 | {"action": "partialUpdateObject", "body": {"objectID": "two_bis", "key": "value"}}, 491 | {"action": "partialUpdateObjectNoCreate", "body": {"objectID": "three", "k": "v"}}, 492 | {"action": "deleteObject", "body": {"objectID": "four"}} 493 | ] 494 | ``` 495 | 496 | * Browse the entire index using **browseObjects** and check that it exactly contains the following objects 497 | 498 | ``` 499 | [ 500 | {"objectID": "zero", "key": "value"}, 501 | {"objectID": "one", "k": "v"}, 502 | {"objectID": "two", "key": "value", "k": "v"}, 503 | {"objectID": "two_bis", "key": "value"}, 504 | {"objectID": "three", "key": "value", "k": "v"}, 505 | {"objectID": "five", "key": "value"}, 506 | ] 507 | ``` 508 | 509 | #### Replacing 510 | 511 | * Instantiate the client and index `replacing` 512 | * Add the following record to the index using **saveObject** and collect the taskID 513 | 514 | ``` 515 | {"objectID": "one"} 516 | ``` 517 | 518 | * Add the following rule to the index using **saveRule** and collect the taskID 519 | 520 | ``` 521 | { 522 | "objectID": "one", 523 | "condition": { "anchoring": "is", "pattern": "pattern"}, 524 | "consequence": { 525 | "params": { 526 | "query": { 527 | "edits": [ 528 | {"type": "remove", "delete": "pattern"} 529 | ] 530 | } 531 | } 532 | } 533 | } 534 | ``` 535 | 536 | * Add the following synonym to the index using **saveSynonym** and collect the taskID 537 | 538 | ``` 539 | {"objectID": "one", "type": "synonym", "synonyms": ["one", "two"]} 540 | ``` 541 | 542 | * Wait for all the previous tasks to terminate using **waitTask** 543 | * Replace the existing object with the following one using **replaceAllObjects** and collect the taskID 544 | 545 | ``` 546 | {"objectID": "two"} 547 | ``` 548 | 549 | * Replace the existing rule with the following one using **replaceAllRules** and collect the taskID 550 | 551 | ``` 552 | { 553 | "objectID": "two", 554 | "condition": { "anchoring": "is", "pattern": "pattern"}, 555 | "consequence": { 556 | "params": { 557 | "query": { 558 | "edits": [ 559 | {"type": "remove", "delete": "pattern"} 560 | ] 561 | } 562 | } 563 | } 564 | } 565 | ``` 566 | 567 | * Replace the existing synonym with the following one using **replaceAllSynonyms** and collect the taskID 568 | 569 | ``` 570 | {"objectID": "two", "type": "synonym", "synonyms": ["one", "two"]} 571 | ``` 572 | 573 | * Wait for all the previous tasks to terminate using **waitTask** 574 | * Check that record with `objectID="one"` doesn’t exist using **getObject** 575 | * Check that record with `objectID="two"` does exist using **getObject** 576 | * Check that rule with `objectID="one"` doesn’t exist using **getRule** 577 | * Check that rule with `objectID="two"` does exist using **getRule** 578 | * Check that synonym with `objectID="one"` doesn’t exist using **getSynonym** 579 | * Check that synonym with `objectID="two"` does exist using **getSynonym** 580 | 581 | #### Exists 582 | 583 | * Instantiate the client and index `exists` 584 | * Check that the index doesn't exist using **exists** 585 | * Save an object to the index and wait for its completion using **waitTask** 586 | * Check that the index exists using **exists**& 587 | * Delete the index using **delete** and wait for this task to complete using the **waitTask** 588 | * Check that the index doesn't exist using **exists** 589 | --- 590 | 591 | ### Tests (client) 592 | 593 | #### Copy index 594 | 595 | * Instantiate the client and index `copy_index` and keep the generated index name (which will be used as a prefix of the copied/moved indices) 596 | * Add the following records with **saveObjects** and collect the taskID 597 | 598 | ``` 599 | [ 600 | {"objectID": "one", "company": "apple"}, 601 | {"objectID": "two", "company": "algolia"} 602 | ] 603 | ``` 604 | 605 | * Set the following settings with **setSettings** and collect the taskID 606 | 607 | ``` 608 | { 609 | "attributesForFaceting": ["company"] 610 | } 611 | ``` 612 | 613 | * Add the following synonym with **saveSynonym** and collect the taskID 614 | 615 | ``` 616 | { 617 | "objectID": "google_placeholder", 618 | "type": "placeholder", 619 | "placeholder": "", 620 | "replacements": ["Google", "GOOG"] 621 | } 622 | ``` 623 | 624 | * Add the following query rule with **saveRule** and collect the taskID 625 | 626 | ``` 627 | { 628 | "objectID": "company_auto_faceting", 629 | "condition": { 630 | "anchoring":"contains", 631 | "pattern":"{facet:company}", 632 | }, 633 | "consequence": { 634 | "params": {"automaticFacetFilters": ["company"]} 635 | } 636 | } 637 | ``` 638 | 639 | * Wait for the collected tasks to terminate with **waitTask** 640 | * Copy `copy_index` index' settings to a new `copy_index_settings` index using **copySettings** and collect the taskID 641 | * Copy `copy_index` index' rules to a new `copy_index_rules` index using **copyRules** and collect the taskID 642 | * Copy `copy_index` index' synonyms to a new `copy_index_synonyms` index using **copySynonyms** and collect the taskID 643 | * Fully copy `copy_index` index into `copy_index_full_copy` with **copyIndex** and collect taskID 644 | * Wait for the collected tasks to terminate with **waitTask** 645 | * Check that `copy_index_settings` only contains the same settings as the original index with **getSettings** 646 | * Check that `copy_index_rules` only contains the same rules as the original index with **getRule** 647 | * Check that `copy_index_synonyms` only contains the same synonyms as the original index with **getSynonym** 648 | * Check that `copy_index_full_copy` contains both the same settings, rules and synonyms as the original index with **getSettings**, **getRule** and **getSynonym** 649 | 650 | #### Multi Cluster Management (MCM) 651 | 652 | * Instantiate a new client with the MCM testing application `5QZOBPRNH0` (shared between all API client testing apps) 653 | * Retrieve all the clusters with **listClusters** 654 | * Check that there are at least 2 clusters in the list 655 | * Using the following convention: 656 | 657 | ``` 658 | LANG-DATE-TIME-INSTANCE-USERID 659 | 660 | Where: 661 | * LANG is the API client language (e.g. scala/go/php/etc.) 662 | * DATE corresponds to the current time according to the following format: YYYY-MM-DD 663 | * TIME corresponds to the current time according to the following format: HH-mm-ss 664 | * INSTANCE corresponds to: 665 | * the TRAVIS_JOB_NUMBER environment variable if available, otherwise 666 | * the current username on the system running the test if available, otherwise 667 | * "unknown" 668 | * USERID is the user id 669 | 670 | For instance: python-2019-01-01-10-10-05-unknown-0 671 | ``` 672 | 673 | * Assign the `USERID`=0 with **assignUserID** with the first cluster from the list we just retrieved 674 | * Assign the `USERID`=1 and `USERID`=2 with **assignUserIDs** with the first cluster from the list we just retrieved 675 | * Loop until the assigned userIDs are found using **getUserID** 676 | * Search of the assigned userIDs using **searchUserIDs** 677 | * List all userIDs using **listUserIDs** and check that the result set is not empty 678 | * Retrieve the top10 userIDs using **getTopUserIDs** and check that the result set is not empty 679 | * Remove the assigned userIDs using **removeUserID** (loop until the call succeeds, this is the only way to make this work) 680 | * Loop until the removed userID cannot be found anymore using **getUserID** (loop until the call succeeds, this is the only way to make this work) 681 | * Perform a **hasPendingMappings** with `retrieve_mappings = true` and ensure that the request is done without errors and that the response is not null/empty. 682 | 683 | #### API keys 684 | 685 | * Instantiate the client 686 | * Create a new API key using **addAPIKey** with the following ACL and parameters and collect the returned key value (its ID actually) 687 | 688 | | Parameter | Value | 689 | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 690 | | acl | ["search"] | 691 | | params | { "description": "A description", "indexes": "index"], "maxHitsPerQuery": 1000, "maxQueriesPerIPPerHour": 1000, "queryParameters": "typoTolerance=strict", "referers": ["referer"], "validity": 600 } | 692 | 693 | * Optionally, defer the deletion of the key using **deleteAPIKey** at the end of your test (using tearDown/afterTest-like methods) if possible with the testing framework of the language. This extra step guarantee that if the test fail before step 8., the inserted key will still be removed. 694 | * Retrieve the added key using **getAPIKey** with the key value that was collected earlier (loop until the call succeeds, this is the only way to make this work) and check that the ACL and params do match (expect for `createdAt` field that was generated and `validity` that may have decreased since the key was inserted) 695 | * List all the API keys using **listAPIKeys** and check that the added key is found as well 696 | * Update the `maxHitsPerQuery` field to 42 of the key using **updateAPIKey** 697 | * Retrieve the added key using **getAPIKey** with the key value that was collected earlier (loop until the call succeeds, this is the only way to make this work) and check that the `maxHitsPerQuery` field of the returned key has indeed changed 698 | * Remove the key using **deleteAPIKey** 699 | * Loop until the removed key gets restored using **restoreApiKey** method. (loop until the call succeeds, this is the only way to make this work) 700 | * Retrieve the restored key using **getAPIKey** with the key value that was collected earlier (loop until the call succeeds, this is the only way to make this work) 701 | * Remove the key using **deleteAPIKey** 702 | 703 | #### Get logs 704 | 705 | * Instantiate the client 706 | * Perform 2 times a **listIndices** operation to ensure at least two logs will be available from the application 707 | * Retrieve the logs with the following parameters using **getLogs**: 708 | 709 | ``` 710 | { 711 | "length": 2, 712 | "offset": 0, 713 | "type": "all" 714 | } 715 | ``` 716 | 717 | * Ensure that the length of the retrieved logs is equal to 2 718 | 719 | #### Multiple Operations 720 | 721 | * Instantiate the client and index1 `multiple_operations` and index2 `multiple_operations_dev` 722 | * Add the following objects using **multipleBatch** to the appropriate index and collect the return objectIDs and taskIDs per index: 723 | 724 | ``` 725 | [ 726 | {"indexName": indexName1, "action": "addObject", "body": {"firstname": "Jimmie"}}, 727 | {"indexName": indexName1, "action": "addObject", "body": {"firstname": "Jimmie"}}, 728 | {"indexName": indexName2, "action": "addObject", "body": {"firstname": "Jimmie"}}, 729 | {"indexName": indexName2, "action": "addObject", "body": {"firstname": "Jimmie"}} 730 | ] 731 | ``` 732 | 733 | * For all the collected taskIDs per index, wait on them using the **waitTask** operation of the appropriate index 734 | * Retrieve all the inserted objects using **multipleGetObjects** from their respective index (the collected objectIDs are in order, meaning that they correspond to the order they were inserted into their respective indices during the **multipleBatch**) and ensure the 4 objects are correctly returned. 735 | * Perform the two following search operations using **multipleQueries** with strategy="none" and ensure that the response contains two results, both containing two hits: 736 | 737 | ``` 738 | [ 739 | {"indexName": indexName1, "params": {"query": "", "hitsPerPage": 2}}, 740 | {"indexName": indexName2, "params": {"query": "", "hitsPerPage": 2}} 741 | ] 742 | ``` 743 | 744 | * Perform the two following search operations using **multipleQueries** with `strategy="stopIfEnoughMatches"` and ensure that the response contains two results, the first one containing two hits and the second none: 745 | 746 | ``` 747 | [ 748 | {"indexName": indexName1, "params": {"query": "", "hitsPerPage": 2}}, 749 | {"indexName": indexName2, "params": {"query": "", "hitsPerPage": 2}} 750 | ] 751 | ``` 752 | 753 | #### DNS timeout 754 | 755 | * Mock the HTTP layer: 756 | * Make a connect timeout last exactly 2 seconds 757 | * Make a 200 respond in exactly 10 milliseconds 758 | * Make any request to a host that doesn't match the pattern `"{appID}-1.algolianet.com"` time out 759 | * Instantiate the client with the regular credentials but provides the following host (in that order): 760 | 761 | ``` 762 | [ 763 | "algolia.biz", 764 | appID + "-1.algolianet.com", 765 | appID + "-2.algolianet.com", 766 | appID + "-3.algolianet.com" 767 | ] 768 | ``` 769 | 770 | * Start a timer 771 | * Perform 10 sequential calls to Algolia using the client’s **listIndices** methods and check that no error happened at each call 772 | * Stop the timer 773 | * Check that the timer’s delta is lower than 3 seconds 774 | 775 | --- 776 | 777 | ### Tests (account) 778 | 779 | #### Copy index 780 | 781 | * Instantiate the client1 and index1 `copy_index` (for app `NOCTT5TZUU`) 782 | * Instantiate the client2 and index2 `copy_index_2` (for app `NOCTT5TZUU`) 783 | * Try to perform an **Account.CopyIndex** from index1 to index2 and make sure that it fails because the application ID is the same 784 | * Re-instantiate the client2 and index2 `copy_index` (for app `UCX3XB3SH4`) 785 | * Add the following record to index1 using **saveObject** and collect the taskID 786 | 787 | ``` 788 | {"objectID": "one"} 789 | ``` 790 | 791 | * Add the following rule to index1 using **saveRule** and collect the taskID 792 | 793 | ``` 794 | { 795 | "objectID": "one", 796 | "condition": { "anchoring": "is", "pattern": "pattern"}, 797 | "consequence": { 798 | "params": { 799 | "query": { 800 | "edits": [ 801 | {"type": "remove", "delete": "pattern"} 802 | ] 803 | } 804 | } 805 | } 806 | } 807 | ``` 808 | 809 | * Add the following synonym to index1 using **saveSynonym** and collect the taskID 810 | 811 | ``` 812 | {"objectID": "one", "type": "synonym", "synonyms": ["one", "two"]} 813 | ``` 814 | 815 | * Set the following settings to index1 using **setSettings** and collect the taskID 816 | 817 | ``` 818 | {"searchableAttributes": ["objectID"]} 819 | ``` 820 | 821 | * Wait for all the previous tasks to terminate using **waitTask** 822 | * Perform a cross-application index copy using **accountClient.copyIndex** from index1 to index2 823 | * Check that record with `objectID="one"` is present on index2 using **getObject** 824 | * Check that rule with `objectID="one"` is present on index2 using **getRule** 825 | * Check that synonym with `objectID="one"` is present on index2 using **getSynonym** 826 | * Check that `searchableAttributes` setting is set to `["objectID"]` on index2 using **getSettings** 827 | * Try to perform an **accountClient.copyIndex** from index1 to index2 and make sure that it fails because the destination index already exists 828 | 829 | --- 830 | 831 | ### Tests (secured API keys) 832 | 833 | #### Generate secured API keys 834 | 835 | * Instantiate the client and index1 `secured_api_keys` 836 | * Instantiate index2 with name `secured_api_keys_dev` 837 | * Add the following object to both indices using **saveObject** and wait for both indexing operations to terminate using **waitTask**: 838 | 839 | ``` 840 | {"objectID": "one"} 841 | ``` 842 | 843 | * Generate a new secured API key using **generateSecuredAPIKey** with the `ALGOLIA_SEARCH_KEY_1` as source key and the following parameters: 844 | 845 | ``` 846 | { 847 | "validUntil": now + 10 min (in epoch seconds), 848 | "restrictIndices": indexName1 849 | } 850 | ``` 851 | 852 | * Instantiate a new client with the generated key 853 | * Perform an empty search operation to index1 using **search** and check that no error happens 854 | * Perform an empty search operation to index2 using **search** and check that an error do happen 855 | 856 | #### Expired Secured API keys 857 | 858 | * Generate a secured API key expiring in 10 minutes 859 | * Call **get_secured_api_key_remaining_validity** with the given secured API key 860 | * Assert that the result is greater than 0s 861 | * Generate a secured API key which has expired 10 minutes ago 862 | * Call **get_secured_api_key_remaining_validity** with the given secured API key 863 | * Assert that the result is lower than 0s 864 | 865 | --- 866 | 867 | ### Tests (analytics) 868 | 869 | #### AB testing 870 | 871 | * Instantiate the client and index1 `ab_testing` and index2 `ab_testing_dev` 872 | * Instantiate the analytics client 873 | * Add the following dummy object to index1 using **saveObject** and wait for this task to complete using the **waitTask** method of index1: 874 | 875 | ``` 876 | {"objectID": "one"} 877 | ``` 878 | 879 | * Add the following dummy object to index2 using **saveObject** and wait for this task to complete using the **waitTask** method of index2: 880 | 881 | ``` 882 | {"objectID": "one"} 883 | ``` 884 | 885 | * Add the following AB test using **addABTest**, collect the returned abTestID from the response and wait for this task to complete using the **waitTask** method of the analytics client: 886 | 887 | ``` 888 | { 889 | "name": abTestName, 890 | "variants": [ 891 | {"index": indexName1, "trafficPercentage": 60, "description": "a description"}, 892 | {"index": indexName2, "trafficPercentage": 40} 893 | ], 894 | "endAt": now + 24h 895 | } 896 | 897 | Where: 898 | * abTestName corresponds to the LANG-DATE-TIME-INSTANCE we already used previously (please refer to other sections of this document) 899 | * indexName1 and indexName2 correspond to the respective names of the generated indices 900 | * now + 24h corresponds to tomorrow’s time at the exact same time (if the API client doesn’t support date/time automatic serialization, the expected format is ISO8601 which roughly corresponds to something like "2006-01-02T15:04:05Z" as a string. 901 | ``` 902 | 903 | * Retrieve the added AB test using **getABTest** thanks to its collected abTestID and check that the retrieved AB test's `name`, `endAt` and variants’ `index`, `trafficPercentage` and `description` fields correspond to the ones of the original AB test (other fields are generated so you shouldn’t have to check them). Finally check that the `status` field of the retrieved AB test is not `stopped`. 904 | * Iterate over all the existing AB tests using **getABTests** and ensure that the added AB test is also found there, using the same comparison as in the previous step 905 | * Stop the AB test using **stopABTest** and wait for this task to complete using the **waitTask** method of the analytics client. 906 | * Retrieve the AB test using **getABTest** and ensure that the `status` field of the AB test is now set to `stopped` 907 | * Remove the AB test using **deleteABTest** and wait for this task to complete using the **waitTask** method of the analytics client 908 | * Ensure that the AB test cannot be retrieved anymore using **getABTest** by expecting an error 909 | 910 | 911 | #### AA testing 912 | 913 | * Instantiate the client and index `aa_testing` 914 | * Instantiate the analytics client 915 | * Add the following dummy object to the index using **saveObject** and wait for this task to complete using the **waitTask** method 916 | 917 | ``` 918 | {"objectID": "one"} 919 | ``` 920 | 921 | * Add the following AB test using **addABTest**, collect the returned abTestID from the response and wait for this task to complete using the **waitTask** method of the analytics client: 922 | 923 | ``` 924 | { 925 | "name": abTestName, 926 | "variants": [ 927 | {"index": indexName, "trafficPercentage": 90}, 928 | {"index": indexName, "trafficPercentage": 10, "customSearchParameters": {"ignorePlurals": true}} 929 | ], 930 | "endAt": now + 24h 931 | } 932 | 933 | Where: 934 | * abTestName corresponds to the LANG-DATE-TIME-INSTANCE we already used previously (please refer to other sections of this document) 935 | * indexName correspond to the name of the generated index 936 | * now + 24h corresponds to tomorrow’s time at the exact same time (if the API client doesn’t support date/time automatic serialization, the expected format is ISO8601 which roughly corresponds to something like "2006-01-02T15:04:05Z" as a string. 937 | ``` 938 | 939 | * Retrieve the added AB test using **getABTest** thanks to its collected abTestID and check that the retrieved AB test’s `name`, `endAt` and variants’ `index`, `trafficPercentage` and `customSearchParameters` fields correspond to the ones of the original AB test (other fields are generated so you shouldn’t have to check them). Finally check that the `status` field of the retrieved AB test is not `stopped`. 940 | 941 | * Remove the added AB test using **deleteABTest** and wait for this task to complete using the **waitTask** method of the analytics client. 942 | 943 | * Try to perform an **getABTest** using the removed AB test and make sure that it fails because the AB test do not exists anymore. 944 | 945 | --- 946 | 947 | ### Tests (insights) 948 | 949 | #### Sending events 950 | 951 | * Instantiate the client and index `sending_events` 952 | * Instantiate the insights client 953 | * Add the following dummy objects to index using **save_objects** and wait for this task to complete using the **wait_task**: 954 | 955 | ``` 956 | [ 957 | {"objectID": "one"}, 958 | {"objectID": "two"} 959 | ] 960 | ``` 961 | 962 | * Perform a **send_event** with the following parameters: 963 | 964 | ``` 965 | { 966 | "eventType": "click", 967 | "eventName": "foo", 968 | "index": , 969 | "userToken": "bar", 970 | "objectIDs": ["one", "two"], 971 | "timestamp": 972 | } 973 | ``` 974 | 975 | * Perform a **send_events** with the following parameters: 976 | 977 | ``` 978 | { 979 | "events": [ 980 | { 981 | "eventType": "click", 982 | "eventName": "foo", 983 | "index": , 984 | "userToken": "bar", 985 | "objectIDs": ["one", "two"], 986 | "timestamp": 987 | }, 988 | { 989 | "eventType": "click", 990 | "eventName": "foo", 991 | "index": , 992 | "userToken": "bar", 993 | "objectIDs": ["one", "two"], 994 | "timestamp": 995 | } 996 | ] 997 | } 998 | ``` 999 | 1000 | * Instantiate a new `insights_user_client` with `insights_client.user("bar")` 1001 | * Using the `insights_user_client`, perform a **clicked_object_ids** with the following parameters: 1002 | 1003 | | Parameter | Value | 1004 | | ----------- | ---------------- | 1005 | | `eventName` | `"foo"` | 1006 | | `index` | `` | 1007 | | `objectIDs` | `["one", "two"]` | 1008 | 1009 | * Perform a search query using **search** with an empty query and `clickAnalytics=true` and get back the `queryID` field from the response 1010 | * Using the `insights_user_client`, perform a **clicked_object_ids_after_search** with the following parameters: 1011 | 1012 | | Parameter | Value | 1013 | | ----------- | ---------------- | 1014 | | `eventName` | `"foo"` | 1015 | | `index` | `` | 1016 | | `objectIDs` | `["one", "two"]` | 1017 | | `positions` | `[1, 2]` | 1018 | | `queryID` | `` | 1019 | 1020 | * Using the `insights_user_client`, perform a **clicked_filters** with the following parameters: 1021 | 1022 | | Parameter | Value | 1023 | | ----------- | ------------------------------ | 1024 | | `eventName` | `"foo"` | 1025 | | `index` | `` | 1026 | | `filters` | `["filter:foo", "filter:bar"]` | 1027 | 1028 | * Using the `insights_user_client`, perform a **converted_object_ids** with the following parameters: 1029 | 1030 | | Parameter | Value | 1031 | | ----------- | ---------------- | 1032 | | `eventName` | `"foo"` | 1033 | | `index` | `` | 1034 | | `objectIDs` | `["one", "two"]` | 1035 | 1036 | * Using the `insights_user_client`, perform a **converted_object_ids_after_search** with the following parameters: 1037 | 1038 | | Parameter | Value | 1039 | | ----------- | ---------------- | 1040 | | `eventName` | `"foo"` | 1041 | | `index` | `` | 1042 | | `objectIDs` | `["one", "two"]` | 1043 | | `positions` | `[1, 2]` | 1044 | | `queryID` | `` | 1045 | 1046 | * Using the `insights_user_client`, perform a **converted_filters** with the following parameters: 1047 | 1048 | | Parameter | Value | 1049 | | ----------- | ------------------------------ | 1050 | | `eventName` | `"foo"` | 1051 | | `index` | `` | 1052 | | `filters` | `["filter:foo", "filter:bar"]` | 1053 | 1054 | * Using the `insights_user_client`, perform a **viewed_object_ids** with the following parameters: 1055 | 1056 | | Parameter | Value | 1057 | | ----------- | ---------------- | 1058 | | `eventName` | `"foo"` | 1059 | | `index` | `` | 1060 | | `objectIDs` | `["one", "two"]` | 1061 | 1062 | * Using the `insights_user_client`, perform a **viewed_filters** with the following parameters: 1063 | 1064 | | Parameter | Value | 1065 | | ----------- | ------------------------------ | 1066 | | `eventName` | `"foo"` | 1067 | | `index` | `` | 1068 | | `filters` | `["filter:foo", "filter:bar"]` | 1069 | 1070 | --- 1071 | 1072 | ### Tests (personalization) 1073 | 1074 | #### Personalization Strategy 1075 | 1076 | * Instantiate the personalization client 1077 | * Perform a **setPersonalizationStrategy** call with the following strategy but do not consider a `{"status":429,"message":"Number of strategy saves exceeded for the day"}` as an error since this is a rate limit we cannot change on the Personalization side. 1078 | ``` 1079 | { 1080 | eventsScoring: [ 1081 | { eventName: 'Add to cart', eventType: 'conversion', score: 50 }, 1082 | { eventName: 'Purchase', eventType: 'conversion', score: 100 }, 1083 | ], 1084 | facetsScoring: [ 1085 | { facetName: 'brand', score: 100 }, 1086 | { facetName: 'categories', score: 10 }, 1087 | ], 1088 | personalizationImpact: 0, 1089 | } 1090 | ``` 1091 | * Perform a **getPersonalizationStrategy** call and ensure it does return the same strategy used on **setPersonalizationStrategy**. 1092 | 1093 | --- 1094 | 1095 | ### Tests (backward compatibility) 1096 | 1097 | The following tests are a bit special in the sense that they could not be 1098 | implemented for every API client as of today. Our API clients need to reflect 1099 | the changes of the actual Algolia REST API. However, we do not want to ask our 1100 | users to cope with breaking changes every time they update their clients. This 1101 | is why we implement breaking changes in a forward compatible manner. Those 1102 | tests are there to make sure we hide the complexity of non-breaking-compatible 1103 | changes to the final users while still offering the very last features, as 1104 | tested by the rest of the Common Test Suite. 1105 | 1106 | #### Old settings 1107 | 1108 | Try to deserialize the following JSON string of settings into the setting 1109 | representation of the API client and ensure they map to the new correct name: 1110 | 1111 | The following JSON string representation of a settings map: 1112 | 1113 | ``` 1114 | { 1115 | "attributesToIndex": ["attr1", "attr2"], 1116 | "numericAttributesToIndex": ["attr1", "attr2"], 1117 | "slaves": ["index1", "index2"] 1118 | } 1119 | ``` 1120 | 1121 | Should deserialize to a settings object like: 1122 | 1123 | ``` 1124 | Settings { 1125 | SearchableAttributes = ["attr1", "attr2"], 1126 | NumericAttributesForFiltering = ["attr1", "attr2"], 1127 | Replicas = ["index1", "index2"], 1128 | } 1129 | ``` 1130 | 1131 | 1132 | #### Query rules v1 1133 | 1134 | The following JSON string representation of a query rule v1: 1135 | 1136 | ``` 1137 | { 1138 | "objectID": "query_edits", 1139 | "condition": {"anchoring": "is", "pattern": "mobile phone"}, 1140 | "consequence": { 1141 | "params": { 1142 | "query": { 1143 | "remove": ["mobile", "phone"] 1144 | } 1145 | } 1146 | } 1147 | } 1148 | ``` 1149 | 1150 | Should deserialize to an query rule v2 object like: 1151 | 1152 | ``` 1153 | Rule { 1154 | ObjectID = "query_edits", 1155 | Condition = { 1156 | Anchoring = "is", 1157 | Pattern = "mobile phone", 1158 | }, 1159 | Consequence = { 1160 | Params = { 1161 | Query = { 1162 | Edits = [ 1163 | Edit{Type="remove", Delete="mobile"}, 1164 | Edit{Type="remove", Delete="phone"}, 1165 | ] 1166 | } 1167 | } 1168 | } 1169 | } 1170 | ``` 1171 | -------------------------------------------------------------------------------- /internals/README.md: -------------------------------------------------------------------------------- 1 | # Internals and Implementation Details 2 | 3 | - [Retry Strategy](retry_strategy.md) 4 | - [`search_client.get_secured_api_key_remaining_validity` function](search_client_get_secured_api_key_remaining_validity.md) 5 | - [`search_index.exists` function](search_index_exists.md) 6 | -------------------------------------------------------------------------------- /internals/retry_strategy.md: -------------------------------------------------------------------------------- 1 | # Retry strategy 2 | 3 | - Lets us test the retry strategy as a separate component 4 | - No network involved so it could easily be unit-tested 5 | 6 | ```java 7 | function request(method, path, body, call_kind, request_options) return body_response { 8 | 9 | for host in retry_strategy.get_tryable_hosts(call_kind) { 10 | request = build_request(method, host.host, path, body, request_options) 11 | response = http_requester.request(request, connect_timeout, write_timeout) 12 | outcome = retry_strategy.decide(response) 13 | 14 | switch outcome { 15 | case success -> return response.body 16 | case failure -> return response.body 17 | } 18 | 19 | // Do not handle the retry case in the switch, 20 | // so that we will loop to the next tryable_host. 21 | } 22 | 23 | // Having looped without early returning with neither 24 | // a success or a failure, it means all tryable hosts 25 | // have been exhausted. Hence an error has to be raised. 26 | return exhausted_hosts 27 | } 28 | 29 | interface retry_strategy { 30 | function get_retryable_hosts(call_kind) return [tryable_host] 31 | function decide(response) return outcome 32 | function set_timeouts(read_timeout, write_timeout) 33 | } 34 | 35 | struct tryable_host { 36 | host (string) 37 | timeout (duration) 38 | } 39 | 40 | enum call_kind { 41 | read 42 | write 43 | } 44 | 45 | enum outcome { 46 | success 47 | failure 48 | retry 49 | } 50 | 51 | interface http_requester { 52 | function request(request, connect_timeout, total_timeout) return response 53 | } 54 | 55 | struct request { 56 | http_method (string) 57 | uri (string) 58 | headers (map) 59 | body (string or reader) 60 | } 61 | 62 | struct response { 63 | http_code (int) 64 | body (string or reader) 65 | is_timeout_error (bool) 66 | is_network_error (bool) 67 | error (string or reader) 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /internals/search_client_get_secured_api_key_remaining_validity.md: -------------------------------------------------------------------------------- 1 | # `search_client.get_secured_api_key_remaining_validity` function 2 | 3 | ## Context 4 | 5 | When a secured API key expires, search breaks. The workaround is more complex 6 | than it needs to be. The options for developers using Algolia require, at every 7 | place where a search operation is performed, catching an operation error, 8 | identify it as a `valid_until` error, then regenerate the API key, and perform 9 | the search operation again. 10 | 11 | The idea of `get_secured_api_key_remaining_validity` is a first step to address 12 | this issue. This function tells if a secured API key is expired or not without 13 | making any network call to the Algolia's API. This function may be called 14 | before instantiating a new `search_client` or before performing a 15 | `search_index.search`. Depending on the result, the user can decide to 16 | regenerate on the fly a secured API key to avoid the search to break live. 17 | 18 | **Note:** For the JavaScript client, it's recommended to regenerate the secured 19 | API key on server side to avoid security breach, since everyone is able to 20 | modify the filters by modifying the code from the browser. 21 | 22 | ## Implementation 23 | 24 | ```ts 25 | interface search_client { 26 | 27 | // get_secured_api_key_remaining_validity takes a secured API key (which is a 28 | // base64-encoded string) as a parameter and return an integer (or a duration if 29 | // the language supports it) telling how many seconds are left before the key 30 | // expiration. 31 | function get_secured_api_key_remaining_validity(secured_api_key: string) return int // or a duration if the language supports it 32 | { 33 | // 1. Check if the secured API key is valid base64 (not null, no whitespace, etc.) 34 | // 2. Decode the base64-encoded string 35 | // 3. Find with a regex the pattern `validUntil=XXX` where `XXX` is a 36 | // valid UNIX epoch timestamp (in seconds) 37 | // 4. If no `validUntil` is found, return the following error: 38 | // "no `validUntil` parameter found, please make sure the secured API key has one" 39 | // 5. Otherwise, extract the `XXX` UNIX timestamp (in seconds) and return 40 | // the following evaluation: `XXX - time.now` 41 | } 42 | 43 | } 44 | ``` 45 | 46 | ## Test plan 47 | 48 | - [CTS test](../common-test-suite#expired-secured-api-keys) 49 | -------------------------------------------------------------------------------- /internals/search_index_exists.md: -------------------------------------------------------------------------------- 1 | # `search_index.exists` function 2 | 3 | ## Context 4 | 5 | Because saving an object will either create a new index (if it didn't exist) or 6 | save into an existing index, we don't have any method to ensure we're working 7 | on an index that doesn't exist. 8 | 9 | The idea is to define a method which tells whether a `search_index` exists or 10 | not, at a given time. Under the hood, we would perform a `get_settings` and 11 | catch the 404 response from the engine. 12 | 13 | ## Implementation 14 | 15 | ```ts 16 | interface search_index { 17 | 18 | // exists return a boolean telling whether the index exists or not. 19 | function exists() return bool 20 | { 21 | // 1. Perform a `get_settings` on the index 22 | // 2. If the engine answered with a 404, return `false`, otherwise return 23 | // `true` 24 | // 3. If a network issue occured, bubble up the error to the user 25 | // (exception or error value depending on the language) 26 | } 27 | 28 | } 29 | ``` 30 | 31 | ## Test plan 32 | 33 | - [CTS test](../common-test-suite#exists) 34 | --------------------------------------------------------------------------------