├── .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 |
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 |
--------------------------------------------------------------------------------