├── .circleci └── config.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── fixtures ├── host-c5s2mj.api.swiftype.com-443 │ ├── 401 │ ├── 404 │ ├── 15193573244029391 │ ├── 151935732448113219 │ ├── 151935732459996374 │ ├── 151935732470971687 │ ├── 15208899348639621 │ ├── 152643363747751014 │ ├── 152643460487616422 │ ├── 152643531437267705 │ ├── 152643588771342928 │ ├── 152666640463285327 │ ├── 154775139235186519 │ ├── 154775139267473451 │ ├── 154775207819050958 │ ├── 154775369860896529 │ ├── 154775388696063661 │ ├── 154775411564681581 │ ├── 154775710380210466 │ ├── 154844848285366145 │ ├── 154905605865969485 │ ├── 154905668608952844 │ ├── 158153945918596904 │ ├── 158646044979291659 │ ├── 158646109175536388 │ └── multiSearchErrors └── localhost-3002 │ ├── 15815391173475749 │ ├── 15815978342879229 │ ├── 158159799829857287 │ ├── get-schema │ └── update-schema ├── lib ├── appSearch.js └── client.js ├── logo-app-search.png ├── package.json └── test └── test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | package-lock.json 60 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Elasticsearch B.V. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic App Search Node client. 2 | Copyright 2012-2019 Elasticsearch B.V. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **⚠️ This client is deprecated ⚠️** 2 | > 3 | > As of Enterprise Search version 8.3.2, we are directing users to the new [Enterprise Search Node Client](https://github.com/elastic/enterprise-search-js) and 4 | > deprecating this client. 5 | > 6 | > Our development effort on this project will be limited to bug fixes. 7 | > All future enhancements will be focused on the Enterprise Search Node Client. 8 | > 9 | > Thank you! - Elastic 10 | 11 |

Elastic App Search Logo

12 | 13 |

CircleCI build

14 | 15 | > A first-party Node.JS client for building excellent, relevant search experiences with [Elastic App Search](https://www.elastic.co/products/app-search). 16 | 17 | ## Contents 18 | 19 | - [Getting started](#getting-started-) 20 | - [Versioning](#versioning) 21 | - [Usage](#usage) 22 | - [Running tests](#running-tests) 23 | - [FAQ](#faq-) 24 | - [Contribute](#contribute-) 25 | - [License](#license-) 26 | 27 | --- 28 | 29 | ## Getting started 🐣 30 | 31 | To install this package, run: 32 | 33 | ```bash 34 | npm install @elastic/app-search-node 35 | ``` 36 | 37 | ## Versioning 38 | 39 | This client is versioned and released alongside App Search. 40 | 41 | To guarantee compatibility, use the most recent version of this library within the major version of the corresponding App Search implementation. 42 | 43 | For example, for App Search `7.3`, use `7.3` of this library or above, but not `8.0`. 44 | 45 | If you are using the [SaaS version available on swiftype.com](https://app.swiftype.com/as) of App Search, you should use the version 7.5.x of the client. 46 | 47 | ## Usage 48 | 49 | ### Setup: Configuring the client and authentication 50 | 51 | Using this client assumes that you have already an instance of [Elastic App Search](https://www.elastic.co/products/app-search) up and running. 52 | 53 | The client is configured using the `baseUrlFn` and `apiKey` parameters. 54 | 55 | ```javascript 56 | const apiKey = 'private-mu75psc5egt9ppzuycnc2mc3' 57 | const baseUrlFn = () => 'http://localhost:3002/api/as/v1/' 58 | const client = new AppSearchClient(undefined, apiKey, baseUrlFn) 59 | ``` 60 | 61 | Note: 62 | 63 | The `[apiKey]` authenticates requests to the API. 64 | You can use any key type with the client, however each has a different scope. 65 | For more information on keys, check out the [documentation](https://swiftype.com/documentation/app-search/api/credentials). 66 | 67 | #### Swiftype.com App Search users: 68 | 69 | When using the [SaaS version available on swiftype.com](https://app.swiftype.com/as) of App Search, you can configure the client using your `hostIdentifier` instead of the `baseUrlFn` parameter. 70 | The `hostIdentifier` can be found within the [Credentials](https://app.swiftype.com/as#/credentials) menu. 71 | 72 | ```javascript 73 | const AppSearchClient = require('@elastic/app-search-node') 74 | const hostIdentifier = 'host-c5s2mj' 75 | const apiKey = 'private-mu75psc5egt9ppzuycnc2mc3' 76 | const client = new AppSearchClient(hostIdentifier, apiKey) 77 | ``` 78 | 79 | ### API Methods 80 | 81 | ##### Indexing: Creating or Replacing Documents 82 | 83 | ```javascript 84 | const engineName = 'favorite-videos' 85 | const documents = [ 86 | { 87 | id: 'INscMGmhmX4', 88 | url: 'https://www.youtube.com/watch?v=INscMGmhmX4', 89 | title: 'The Original Grumpy Cat', 90 | body: 'A wonderful video of a magnificent cat.' 91 | }, 92 | { 93 | id: 'JNDFojsd02', 94 | url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 95 | title: 'Another Grumpy Cat', 96 | body: 'A great video of another cool cat.' 97 | } 98 | ] 99 | 100 | client 101 | .indexDocuments(engineName, documents) 102 | .then(response => console.log(response)) 103 | .catch(error => console.log(error)) 104 | ``` 105 | 106 | Note that this API will not throw on an indexing error. Errors are inlined in the response body per document: 107 | 108 | ```json 109 | [ 110 | { "id": "park_rocky-mountain", "errors": [] }, 111 | { 112 | "id": "park_saguaro", 113 | "errors": ["Invalid field value: Value 'foo' cannot be parsed as a float"] 114 | } 115 | ] 116 | 117 | ``` 118 | 119 | ##### Indexing: Updating Documents (Partial Updates) 120 | 121 | ```javascript 122 | const engineName = 'favorite-videos' 123 | const documents = [ 124 | { 125 | id: 'INscMGmhmX4', 126 | title: 'Updated title' 127 | } 128 | ] 129 | 130 | client 131 | .updateDocuments(engineName, documents) 132 | .then(response => console.log(response)) 133 | .catch(error => console.log(error)) 134 | ``` 135 | 136 | 137 | ##### Retrieving Documents 138 | 139 | ```javascript 140 | const engineName = 'favorite-videos' 141 | const documentIds = ['INscMGmhmX4', 'JNDFojsd02'] 142 | 143 | client 144 | .getDocuments(engineName, documentIds) 145 | .then(response => console.log(response)) 146 | .catch(error => console.log(error.errorMessages)) 147 | ``` 148 | 149 | ##### Listing Documents 150 | 151 | ```javascript 152 | const engineName = 'favorite-videos' 153 | 154 | // Without paging 155 | client 156 | .listDocuments(engineName) 157 | .then(response => console.log(response)) 158 | .catch(error => console.log(error.errorMessages)) 159 | 160 | // With paging 161 | client 162 | .listDocuments(engineName, { page: { size: 10, current: 1 } }) 163 | .then(response => console.log(response)) 164 | .catch(error => console.log(error.errorMessages)) 165 | ``` 166 | 167 | ##### Destroying Documents 168 | 169 | ```javascript 170 | const engineName = 'favorite-videos' 171 | const documentIds = ['INscMGmhmX4', 'JNDFojsd02'] 172 | 173 | client 174 | .destroyDocuments(engineName, documentIds) 175 | .then(response => console.log(response)) 176 | .catch(error => console.log(error.errorMessages)) 177 | ``` 178 | 179 | ##### Listing Engines 180 | 181 | ```javascript 182 | client 183 | .listEngines({ page: { size: 10, current: 1 } }) 184 | .then(response => console.log(response)) 185 | .catch(error => console.log(error.errorMessages)) 186 | ``` 187 | 188 | ##### Retrieving Engines 189 | 190 | ```javascript 191 | const engineName = 'favorite-videos' 192 | 193 | client 194 | .getEngine(engineName) 195 | .then(response => console.log(response)) 196 | .catch(error => console.log(error.errorMessages)) 197 | ``` 198 | 199 | ##### Creating Engines 200 | 201 | ```javascript 202 | const engineName = 'favorite-videos' 203 | 204 | client 205 | .createEngine(engineName, { language: 'en' }) 206 | .then(response => console.log(response)) 207 | .catch(error => console.log(error.errorMessages)) 208 | ``` 209 | 210 | ##### Destroying Engines 211 | 212 | ```javascript 213 | const engineName = 'favorite-videos' 214 | 215 | client 216 | .destroyEngine(engineName) 217 | .then(response => console.log(response)) 218 | .catch(error => console.log(error.errorMessages)) 219 | ``` 220 | 221 | ##### Searching 222 | 223 | ```javascript 224 | const engineName = 'favorite-videos' 225 | const query = 'cat' 226 | const searchFields = { title: {} } 227 | const resultFields = { title: { raw: {} } } 228 | const options = { search_fields: searchFields, result_fields: resultFields } 229 | 230 | client 231 | .search(engineName, query, options) 232 | .then(response => console.log(response)) 233 | .catch(error => console.log(error.errorMessages)) 234 | ``` 235 | 236 | ##### Multi-Search 237 | 238 | ```javascript 239 | const engineName = 'favorite-videos' 240 | const searches = [ 241 | { query: 'cat', options: { 242 | search_fields: { title: {} }, 243 | result_fields: { title: { raw: {} } } 244 | } }, 245 | { query: 'grumpy', options: {} } 246 | ] 247 | 248 | client 249 | .multiSearch(engineName, searches) 250 | .then(response => console.log(response)) 251 | .catch(error => console.log(error.errorMessages)) 252 | ``` 253 | 254 | ##### Query Suggestion 255 | 256 | ```javascript 257 | const engineName = 'favorite-videos' 258 | const options = { 259 | size: 3, 260 | types: { 261 | documents: { 262 | fields: ['title'] 263 | } 264 | } 265 | } 266 | 267 | client 268 | .querySuggestion(engineName, 'cat', options) 269 | .then(response => console.log(response)) 270 | .catch(error => console.log(error.errorMessages)) 271 | ``` 272 | 273 | ##### Listing Curations 274 | 275 | ```javascript 276 | const engineName = 'favorite-videos' 277 | 278 | client 279 | .listCurations(engineName) 280 | .then(response => console.log(response)) 281 | .catch(error => console.log(error.errorMessages)) 282 | 283 | // Pagination details are optional 284 | const paginationDetails = { 285 | page: { 286 | current: 2, 287 | size: 10 288 | } 289 | } 290 | 291 | client 292 | .listCurations(engineName, paginationDetails) 293 | .then(response => console.log(response)) 294 | .catch(error => console.log(error.errorMessages)) 295 | ``` 296 | 297 | ##### Retrieving Curations 298 | 299 | ```javascript 300 | const engineName = 'favorite-videos' 301 | const curationId = 'cur-7438290' 302 | 303 | client 304 | .getCuration(engineName, curationId) 305 | .then(response => console.log(response)) 306 | .catch(error => console.log(error.errorMessages)) 307 | ``` 308 | 309 | ##### Creating Curations 310 | 311 | ```javascript 312 | const engineName = 'favorite-videos' 313 | const newCuration = { 314 | queries: ['cat blop'], 315 | promoted: ['Jdas78932'], 316 | hidden: ['INscMGmhmX4', 'JNDFojsd02'] 317 | } 318 | 319 | client 320 | .createCuration(engineName, newCuration) 321 | .then(response => console.log(response)) 322 | .catch(error => console.log(error.errorMessages)) 323 | ``` 324 | 325 | ##### Updating Curations 326 | 327 | ```javascript 328 | const engineName = 'favorite-videos' 329 | const curationId = 'cur-7438290' 330 | // "queries" is required, either "promoted" or "hidden" is required. 331 | // Values sent for all fields will overwrite existing values. 332 | const newDetails = { 333 | queries: ['cat blop'], 334 | promoted: ['Jdas78932', 'JFayf782'] 335 | } 336 | 337 | client 338 | .updateCuration(engineName, curationId, newDetails) 339 | .then(response => console.log(response)) 340 | .catch(error => console.log(error.errorMessages)) 341 | ``` 342 | 343 | ##### Deleting Curations 344 | 345 | ```javascript 346 | const engineName = 'favorite-videos' 347 | const curationId = 'cur-7438290' 348 | 349 | client 350 | .destroyCuration(engineName, curationId) 351 | .then(response => console.log(response)) 352 | .catch(error => console.log(error.errorMessages)) 353 | ``` 354 | 355 | ##### Retrieving Schemas 356 | 357 | ```javascript 358 | const engineName = 'favorite-videos' 359 | 360 | client 361 | .getSchema(engineName) 362 | .then(response => console.log(response)) 363 | .catch(error => console.log(error.errorMessages)) 364 | ``` 365 | 366 | ##### Updating Schemas 367 | 368 | ```javascript 369 | const engineName = 'favorite-videos' 370 | const schema = { 371 | views: 'number', 372 | created_at: 'date' 373 | } 374 | 375 | client 376 | .updateSchema(engineName, schema) 377 | .then(response => console.log(response)) 378 | .catch(error => console.log(error.errorMessages)) 379 | ``` 380 | 381 | ##### Create a Signed Search Key 382 | 383 | Creating a search key that will only return the title field. 384 | 385 | ```javascript 386 | const publicSearchKey = 'search-xxxxxxxxxxxxxxxxxxxxxxxx' 387 | // This name must match the name of the key above from your App Search dashboard 388 | const publicSearchKeyName = 'search-key' 389 | const enforcedOptions = { 390 | result_fields: { title: { raw: {} } }, 391 | filters: { world_heritage_site: 'true' } 392 | } 393 | 394 | // Optional. See https://github.com/auth0/node-jsonwebtoken#usage for all options 395 | const signOptions = { 396 | expiresIn: '5 minutes' 397 | } 398 | 399 | const signedSearchKey = AppSearchClient.createSignedSearchKey( 400 | publicSearchKey, 401 | publicSearchKeyName, 402 | enforcedOptions, 403 | signOptions 404 | ) 405 | 406 | const baseUrlFn = () => 'http://localhost:3002/api/as/v1/' 407 | const client = new AppSearchClient(undefined, signedSearchKey, baseUrlFn) 408 | 409 | client.search('sample-engine', 'everglade') 410 | ``` 411 | 412 | ##### Create a Meta Engine 413 | 414 | ```javascript 415 | const engineName = 'my-meta-engine' 416 | 417 | client 418 | .createMetaEngine(engineName, ['source-engine-1', 'source-engine-2']) 419 | .then(response => console.log(response)) 420 | .catch(error => console.log(error.errorMessages)) 421 | ``` 422 | 423 | ##### Add a Source Engine to a Meta Engine 424 | 425 | ```javascript 426 | const engineName = 'my-meta-engine' 427 | 428 | client 429 | .addMetaEngineSources(engineName, ['source-engine-3']) 430 | .then(response => console.log(response)) 431 | .catch(error => console.log(error.errorMessages)) 432 | ``` 433 | 434 | ##### Remove a Source Engine from a Meta Engine 435 | 436 | ```javascript 437 | const engineName = 'my-meta-engine' 438 | 439 | client 440 | .deleteMetaEngineSources(engineName, ['source-engine-3']) 441 | .then(response => console.log(response)) 442 | .catch(error => console.log(error.errorMessages)) 443 | ``` 444 | 445 | ##### Creating Engines 446 | 447 | ```javascript 448 | const engineName = 'my-meta-engine' 449 | 450 | client 451 | .createEngine(engineName, { 452 | type: 'meta', 453 | source_engines: ['source-engine-1', 'source-engine-2'] 454 | }) 455 | .then(response => console.log(response)) 456 | .catch(error => console.log(error.errorMessages)) 457 | ``` 458 | 459 | ### For App Search APIs not available in this client 460 | 461 | We try to keep this client up to date with all of the available API endpoints available from App Search. 462 | 463 | There are a few APIs that may not be available yet. For those APIs, please use the low-level client to connect to hit any App Search endpoint. 464 | 465 | ```javascript 466 | const engineName = 'favorite-videos' 467 | const options = { 468 | query: 'cats' 469 | } 470 | 471 | const Client = require('@elastic/app-search-node/lib/client') 472 | const client = new Client('private-mu75psc5egt9ppzuycnc2mc3', 'http://localhost:3002/api/as/v1/') 473 | client.post(`engines/${encodeURIComponent(engineName)}/search`, options).then(console.log) 474 | ``` 475 | 476 | ## Running tests 477 | 478 | ```bash 479 | npm test 480 | ``` 481 | 482 | The specs in this project use [node-replay](https://github.com/assaf/node-replay) to capture fixtures. 483 | 484 | New fixtures should be captured from a running instance of App Search. 485 | 486 | To capture new fixtures, run a command like the following: 487 | 488 | ``` 489 | nvm use 490 | HOST_IDENTIFIER=host-c5s2mj API_KEY=private-b94wtaoaym2ovdk5dohj3hrz REPLAY=record npm run test -- -g 'should create a meta engine' 491 | ``` 492 | 493 | To break that down a little... 494 | - `HOST_IDENTIFIER` - Use this to override the fake value used in tests with an actual valid value for your App Search instance to record from 495 | - `API_KEY` - Use this to override the fake value used in tests with an actual valid value for your App Search instance to record from 496 | - `REPLAY=record` - Tells replay to record a new response if one doesn't already exist 497 | - `npm run test` - Run the tests 498 | - `-- -g 'should create a meta engine'` - Limit the tests to ONLY run the new test you've created, 'should create a meta engine' for example 499 | 500 | This will create a new fixture, make sure you manually edit that fixture to replace the host identifier and api key 501 | recorded in that fixture with the values the tests use. 502 | 503 | You'll also need to make sure that fixture is located in the correctly named directory under `fixtures` according to the host that was used. 504 | 505 | You'll know if something is not right because this will error when you run `npm run test` with an error like: 506 | ``` 507 | Error: POST https://host-c5s2mj.api.swiftype.com:443/api/as/v1/engines refused: not recording and no network access 508 | ``` 509 | 510 | 511 | ## FAQ 🔮 512 | 513 | ### Where do I report issues with the client? 514 | 515 | If something is not working as expected, please open an [issue](https://github.com/elastic/app-search-node/issues/new). 516 | 517 | ### Where can I learn more about App Search? 518 | 519 | Your best bet is to read the [documentation](https://swiftype.com/documentation/app-search). 520 | 521 | ### Where else can I go to get help? 522 | 523 | You can checkout the [Elastic App Search community discuss forums](https://discuss.elastic.co/c/app-search). 524 | 525 | ## Contribute 🚀 526 | 527 | We welcome contributors to the project. Before you begin, a couple notes... 528 | 529 | - Prior to opening a pull request, please create an issue to [discuss the scope of your proposal](https://github.com/elastic/app-search-node/issues). 530 | - Please write simple code and concise documentation, when appropriate. 531 | 532 | ## License 📗 533 | 534 | [Apache 2.0](https://github.com/elastic/app-search-node/blob/master/LICENSE.txt) © [Elastic](https://github.com/elastic) 535 | 536 | Thank you to all the [contributors](https://github.com/elastic/app-search-node/graphs/contributors)! 537 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/15193573244029391: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/documents 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [{\"id\":\"INscMGmhmX4\",\"url\":\"http://www.youtube.com/watch?v=v1uyQZNg2vE\",\"title\":\"The Original Grumpy Cat\",\"body\":\"this is a test\"},{\"id\":\"JNDFojsd02\",\"url\":\"http://www.youtube.com/watch?v=tsdfhk2j\",\"title\":\"Another Grumpy Cat\",\"body\":\"this is also a test\"}] 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | content-type: application/json; charset=utf-8 15 | vary: Origin 16 | etag: W/"1c505a964639396d60ef34105952fd98" 17 | cache-control: max-age=0, private, must-revalidate 18 | x-request-id: f8afdd65-b6a4-4fda-bc6d-304ef9d0fafe 19 | x-runtime: 0.124646 20 | connection: close 21 | transfer-encoding: chunked 22 | 23 | [{"id":"INscMGmhmX4","errors":[]},{"id":"JNDFojsd02","errors":[]}] 24 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/151935732448113219: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/documents 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [\"INscMGmhmX4\",\"JNDFojsd02\"] 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | content-type: application/json; charset=utf-8 15 | vary: Origin 16 | etag: W/"fe9687ede417e1dedbe25fdcc23d2e03" 17 | cache-control: max-age=0, private, must-revalidate 18 | x-request-id: 0bc07f29-0ad0-4f6a-b625-129788683fab 19 | x-runtime: 0.062718 20 | connection: close 21 | transfer-encoding: chunked 22 | 23 | [{"url":"http://www.youtube.com/watch?v=v1uyQZNg2vE","title":"The Original Grumpy Cat","body":"this is a test","id":"INscMGmhmX4"},{"url":"http://www.youtube.com/watch?v=tsdfhk2j","title":"Another Grumpy Cat","body":"this is also a test","id":"JNDFojsd02"}] 24 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/151935732459996374: -------------------------------------------------------------------------------- 1 | DELETE /api/as/v1/engines/swiftype-api-example/documents 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [\"INscMGmhmX4\",\"FakeId\"] 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | content-type: application/json; charset=utf-8 15 | vary: Origin 16 | etag: W/"ceced1d361650a5d277df9c5c364a48c" 17 | cache-control: max-age=0, private, must-revalidate 18 | x-request-id: a3cb32e5-caa3-4e36-ac25-3820fcf8ef0e 19 | x-runtime: 0.111995 20 | connection: close 21 | transfer-encoding: chunked 22 | 23 | [{"id":"INscMGmhmX4","deleted":true},{"id":"FakeId","deleted":false}] 24 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/151935732470971687: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/search 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | content-type: application/json; charset=utf-8 15 | vary: Origin 16 | etag: W/"fd34dfceda0904c198699f8354e48ee3" 17 | cache-control: max-age=0, private, must-revalidate 18 | x-request-id: 347bf555-bd81-4ec2-b156-a387ce69354a 19 | x-runtime: 0.101711 20 | connection: close 21 | transfer-encoding: chunked 22 | 23 | {"meta":{"page":{"current":1,"total_pages":1,"total_results":2,"size":10},"request_id":"347bf555-bd81-4ec2-b156-a387ce69354a"},"results":[{"body":{"raw":"this is a test"},"title":{"raw":"The Original Grumpy Cat"},"url":{"raw":"http://www.youtube.com/watch?v=v1uyQZNg2vE"},"id":{"raw":"INscMGmhmX4"},"_meta":{"score":0.42733237}},{"body":{"raw":"this is also a test"},"title":{"raw":"Another Grumpy Cat"},"url":{"raw":"http://www.youtube.com/watch?v=tsdfhk2j"},"id":{"raw":"JNDFojsd02"},"_meta":{"score":0.42733237}}]} 24 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/15208899348639621: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/documents 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [{\"id\":\"INscMGmhmX4\",\"url\":\"http://www.youtube.com/watch?v=v1uyQZNg2vE\",\"title\":\"The Original Grumpy Cat\",\"body\":\"this is a test\"}] 9 | 10 | HTTP/1.1 200 OK 11 | date: Mon, 12 Mar 2018 21:25:29 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"a724d96214a9badc432200ab6d253ac2" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: e47a13e9ba8080ce151027e3195cccc5 23 | x-runtime: 0.146371 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-node: web02.dal05 27 | 28 | [{"id":"INscMGmhmX4","errors":[]}] 29 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/152643363747751014: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | 8 | HTTP/1.1 200 OK 9 | date: Wed, 16 May 2018 01:20:37 GMT 10 | content-type: application/json; charset=utf-8 11 | transfer-encoding: chunked 12 | connection: close 13 | vary: Accept-Encoding, Accept-Encoding, Origin 14 | status: 200 OK 15 | x-frame-options: SAMEORIGIN 16 | x-xss-protection: 1; mode=block 17 | x-content-type-options: nosniff 18 | etag: W/"250d4a4717082f5a2e3b40250c8923f4" 19 | cache-control: max-age=0, private, must-revalidate 20 | x-request-id: ed93b4296c3b369916cecd6ef2aef89c 21 | x-runtime: 0.070549 22 | x-swiftype-datacenter: dal05 23 | x-swiftype-frontend-node: web02.dal05 24 | x-swiftype-edge-node: web02.dal05 25 | 26 | {"meta":{"page":{"current":1,"total_pages":1,"total_results":3,"size":25}},"results":[{"name":"node-modules","type":"default","language":null},{"name":"ruby-gems","type":"default","language":null},{"name":"test-engine","type":"default","language":null}]} 27 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/152643460487616422: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Wed, 16 May 2018 01:36:44 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"b33399d52d37875c034420987d0c9a3f" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 5e06f6a80b733ec4341b5e60b7681bd2 23 | x-runtime: 0.071062 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-node: web02.dal05 27 | 28 | {"name":"swiftype-api-example","type":"default","language":null} 29 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/152643531437267705: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"name\":\"new-engine\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Wed, 16 May 2018 01:48:34 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"36c4e09df35ed4a65dc5d132bd9a6ffb" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 308ddf80b7f98bff62da82fc1270cfb5 23 | x-runtime: 0.347975 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-node: web01.dal05 27 | 28 | {"name":"new-engine","type":"default","language":null} 29 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/152643588771342928: -------------------------------------------------------------------------------- 1 | DELETE /api/as/v1/engines/new-engine 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Wed, 16 May 2018 01:58:07 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"ebaee1a83baef03e1492e5832fc006d0" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 3bb56da4af9688430c5e51955073afa3 23 | x-runtime: 0.131635 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-node: web01.dal05 27 | 28 | {"deleted":true} 29 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/152666640463285327: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines?page[current]=2&page[size]=1 2 | content-type: application/json 3 | host: host-c5s2mj.api.swiftype.com 4 | x-swiftype-client: elastic-app-search-node 5 | x-swiftype-client-version: 8.3.2 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Fri, 18 May 2018 18:00:04 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | etag: W/"8cbec2b26f50c6ff8cf9c94af0593aec" 21 | cache-control: max-age=0, private, must-revalidate 22 | x-request-id: 66e9c1b313caaa0c6105eb538a46e1aa 23 | x-runtime: 0.026786 24 | x-swiftype-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-node: web01.dal05 27 | 28 | {"meta":{"page":{"current":2,"total_pages":3,"total_results":3,"size":1}},"results":[{"name":"ruby-gems","type":"default","language":null}]} 29 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775139235186519: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/curations 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 17 Jan 2019 18:56:32 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 401 Unauthorized 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: c3f4f569d971ad431d1287b3caa52c31 23 | x-runtime: 0.035655 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"page":{"current":1,"total_pages":1,"total_results":2,"size":25}},"results":[{"id":"cur-9382","queries":["mew hiss"],"promoted":["INscMGmhmX4"],"hidden":["JNDFojsd02"]},{"id":"cur-378291","queries":["grrr scratch"],"promoted":["JNDFojsd02"],"hidden":["INscMGmhmX4"]}]} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775139267473451: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/curations?page[current]=2&page[size]=1 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 17 Jan 2019 18:56:32 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | www-authenticate: Basic realm="Swiftype" 21 | cache-control: no-cache 22 | x-request-id: 03632a579705aa38573c9fcbd29c0be2 23 | x-runtime: 0.021305 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"page":{"current":2,"total_pages":2,"total_results":2,"size":1}},"results":[{"id":"cur-378291","queries":["grrr scratch"],"promoted":["JNDFojsd02"],"hidden":["INscMGmhmX4"]}]} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775207819050958: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/curations/cur-9382 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 17 Jan 2019 19:07:58 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 200 OK 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: 401c83bae4b9e9be82acf7366a8a15bd 23 | x-runtime: 0.017829 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"meta":{"page":{"current":1,"total_pages":1,"total_results":1,"size":25}},"results":[{"id":"cur-9382","queries":["mew hiss"],"promoted":["INscMGmhmX4"],"hidden":["JNDFojsd02"]}]} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775369860896529: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/curations 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"queries\":[\"arf arf\"],\"promoted\":[],\"hidden\":[\"INscMGmhmX4\",\"JNDFojsd02\"]} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 17 Jan 2019 19:34:58 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 200 OK 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: ffc905295c0530ef21f71cf5c322ba91 23 | x-runtime: 0.021898 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"id": "cur-9043829"} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775388696063661: -------------------------------------------------------------------------------- 1 | DELETE /api/as/v1/engines/swiftype-api-example/curations/cur-378291 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 17 Jan 2019 19:38:06 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 200 OK 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: 83fb305ee12bfe40282325524789753f 23 | x-runtime: 0.024097 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web01.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web01.dal05 28 | 29 | {"deleted": true} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775411564681581: -------------------------------------------------------------------------------- 1 | PUT /api/as/v1/engines/swiftype-api-example/curations/cur-9382 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"queries\":[\"mew hiss\",\"sideways hop\"],\"promoted\":[\"INscMGmhmX4\"],\"hidden\":[\"JNDFojsd02\"]} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 17 Jan 2019 19:41:55 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 200 OK 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: c7f73cec2a3c5ac9177e93cf6ac32032 23 | x-runtime: 0.020304 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"id": "cur-9382"} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154775710380210466: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/curations/cur-0000 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 404 Not Found 11 | date: Thu, 17 Jan 2019 20:31:43 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 404 Not Found 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: fc44266bdab7628ed1cdef8e6af334b0 23 | x-runtime: 0.030466 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"errors":["Curation not found with id: cur-0000"]} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154844848285366145: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/multi_search 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"queries\":[{\"query\":\"cat\"},{\"query\":\"grumpy\"}]} 9 | 10 | HTTP/1.1 200 OK 11 | date: Fri, 25 Jan 2019 20:34:42 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-ratelimit-limit: 2400 21 | x-ratelimit-remaining: 2397 22 | etag: W/"97487a783361f0beef00632c932f7d96" 23 | cache-control: max-age=0, private, must-revalidate 24 | x-request-id: 40b424eace5b5edd28157be497d52c7b 25 | x-runtime: 0.082590 26 | x-swiftype-frontend-datacenter: dal05 27 | x-swiftype-frontend-node: web01.dal05 28 | x-swiftype-edge-datacenter: dal05 29 | x-swiftype-edge-node: web01.dal05 30 | 31 | [{"meta":{"warnings":[],"page":{"current":1,"total_pages":1,"total_results":2,"size":10}},"results":[{"body":{"raw":"A wonderful video of a magnificent cat."},"title":{"raw":"The Original Grumpy Cat"},"url":{"raw":"https://www.youtube.com/watch?v=INscMGmhmX4"},"id":{"raw":"INscMGmhmX4"},"_meta":{"score":0.459039}},{"body":{"raw":"A great video of another cool cat."},"title":{"raw":"Another Grumpy Cat"},"url":{"raw":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"},"id":{"raw":"JNDFojsd02"},"_meta":{"score":0.4121608}}]},{"meta":{"warnings":[],"page":{"current":1,"total_pages":1,"total_results":2,"size":10}},"results":[{"body":{"raw":"A great video of another cool cat."},"title":{"raw":"Another Grumpy Cat"},"url":{"raw":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"},"id":{"raw":"JNDFojsd02"},"_meta":{"score":0.39919013}},{"body":{"raw":"A wonderful video of a magnificent cat."},"title":{"raw":"The Original Grumpy Cat"},"url":{"raw":"https://www.youtube.com/watch?v=INscMGmhmX4"},"id":{"raw":"INscMGmhmX4"},"_meta":{"score":0.39919013}}]}] 32 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154905605865969485: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/query_suggestion 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Fri, 01 Feb 2019 21:20:58 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-ratelimit-limit: 6000 21 | x-ratelimit-remaining: 5994 22 | etag: W/"527a446e8aab1ba89e4c08f53dbbd470" 23 | cache-control: max-age=0, private, must-revalidate 24 | x-request-id: 7414beb06c644b1aa88accb6019c6d6f 25 | x-runtime: 0.168318 26 | x-swiftype-frontend-datacenter: dal05 27 | x-swiftype-frontend-node: web02.dal05 28 | x-swiftype-edge-datacenter: dal05 29 | x-swiftype-edge-node: web02.dal05 30 | 31 | {"results":{"documents":[{"suggestion":"cat"}]},"meta":{"request_id":"7414beb06c644b1aa88accb6019c6d6f"}} 32 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/154905668608952844: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/query_suggestion 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"size\":3,\"types\":{\"documents\":{\"fields\":[\"title\"]}},\"query\":\"cat\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Fri, 01 Feb 2019 21:31:26 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-ratelimit-limit: 6000 21 | x-ratelimit-remaining: 5999 22 | etag: W/"c6568d07f99643b506ad7a1b1e5c70dd" 23 | cache-control: max-age=0, private, must-revalidate 24 | x-request-id: a0e24c46d4379e58bf871a6a515b8d94 25 | x-runtime: 0.124237 26 | x-swiftype-frontend-datacenter: dal05 27 | x-swiftype-frontend-node: web01.dal05 28 | x-swiftype-edge-datacenter: dal05 29 | x-swiftype-edge-node: web01.dal05 30 | 31 | {"results":{"documents":[{"suggestion":"cat"}]},"meta":{"request_id":"a0e24c46d4379e58bf871a6a515b8d94"}} 32 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/158153945918596904: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"name\":\"new-engine\",\"language\":\"en\"} 9 | 10 | HTTP/1.1 200 OK 11 | date: Wed, 12 Feb 2020 20:30:58 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-swiftype-backend-region: dal 21 | x-swiftype-backend-datacenter: dal10 22 | x-swiftype-backend-node: app-api02b.dal10 23 | x-ratelimit-limit: 15000 24 | x-ratelimit-remaining: 14999 25 | etag: W/"86b23aaaf3bab9826cdf8c71cbc909f2" 26 | cache-control: max-age=0, private, must-revalidate 27 | x-request-id: 047d3cd04240c5599dd99b3692df7a8a 28 | x-runtime: 0.583643 29 | x-swiftype-frontend-datacenter: dal10 30 | x-swiftype-frontend-node: web02b.dal10 31 | x-swiftype-edge-datacenter: dal10 32 | x-swiftype-edge-node: web02b.dal10 33 | 34 | {"name":"new-engine","type":"default","language":"en"} 35 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/158646044979291659: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/documents/list 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 09 Apr 2020 19:27:29 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-swiftype-backend-region: dal 21 | x-swiftype-backend-datacenter: dal10 22 | x-swiftype-backend-node: app-api01a.dal10 23 | x-ratelimit-limit: 15000 24 | x-ratelimit-remaining: 14998 25 | etag: W/"da0db5a8dc938c11da9bd83edab9628b" 26 | cache-control: max-age=0, private, must-revalidate 27 | x-request-id: e495da5964b2e17ede42ee2f58430f54 28 | x-runtime: 0.042386 29 | x-swiftype-frontend-datacenter: dal12 30 | x-swiftype-frontend-node: web01a.dal12 31 | x-swiftype-edge-datacenter: dal12 32 | x-swiftype-edge-node: web01a.dal12 33 | 34 | {"meta":{"page":{"current":1,"total_pages":1,"total_results":3,"size":100}},"results":[{"body":"this is a test","id":"INscMGmhmX4","title":"The Original Grumpy Cat","url":"http://www.youtube.com/watch?v=v1uyQZNg2vE"},{"body":"this is also a test","id":"JNDFojsd02","title":"Another Grumpy Cat","url":"http://www.youtube.com/watch?v=tsdfhk2j"}]} 35 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/158646109175536388: -------------------------------------------------------------------------------- 1 | PATCH /api/as/v1/engines/swiftype-api-example/documents 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [{\"id\":\"INscMGmhmX4\",\"url\":\"http://www.youtube.com/watch?v=v1uyQZNg2vE\",\"title\":\"The Original Grumpy Cat\",\"body\":\"this is a test\"},{\"id\":\"JNDFojsd02\",\"url\":\"http://www.youtube.com/watch?v=tsdfhk2j\",\"title\":\"Another Grumpy Cat\",\"body\":\"this is also a test\"}] 9 | 10 | HTTP/1.1 200 OK 11 | date: Thu, 09 Apr 2020 19:38:11 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 200 OK 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-swiftype-backend-region: dal 21 | x-swiftype-backend-datacenter: dal10 22 | x-swiftype-backend-node: app-api02b.dal10 23 | x-ratelimit-limit: 360000 24 | x-ratelimit-remaining: 359997 25 | etag: W/"1c505a964639396d60ef34105952fd98" 26 | cache-control: max-age=0, private, must-revalidate 27 | x-request-id: ca2fd5ce01ab2f2b29dfbb8bcc3430e8 28 | x-runtime: 0.061411 29 | x-swiftype-frontend-datacenter: dal12 30 | x-swiftype-frontend-node: web01a.dal12 31 | x-swiftype-edge-datacenter: dal12 32 | x-swiftype-edge-node: web01a.dal12 33 | 34 | [{"id":"INscMGmhmX4","errors":[]},{"id":"JNDFojsd02","errors":[]}] 35 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/401: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/search 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer invalid 7 | accept: application/json 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 401 Unauthorized 11 | date: Mon, 28 Jan 2019 15:29:54 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 401 Unauthorized 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | www-authenticate: Basic realm="Swiftype" 20 | vary: Origin 21 | cache-control: no-cache 22 | x-request-id: 35f86e7b2507d8437778666411599f3e 23 | x-runtime: 0.022311 24 | x-swiftype-frontend-datacenter: dal05 25 | x-swiftype-frontend-node: web02.dal05 26 | x-swiftype-edge-datacenter: dal05 27 | x-swiftype-edge-node: web02.dal05 28 | 29 | {"error":"Invalid authentication token."} 30 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/404: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/invalid-engine-name/search 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"query\":\"cat\"} 9 | 10 | HTTP/1.1 404 Not Found 11 | date: Mon, 28 Jan 2019 15:31:36 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | vary: Accept-Encoding, Accept-Encoding, Origin 16 | status: 404 Not Found 17 | x-frame-options: SAMEORIGIN 18 | x-xss-protection: 1; mode=block 19 | x-content-type-options: nosniff 20 | x-ratelimit-limit: 6000 21 | x-ratelimit-remaining: 5999 22 | cache-control: no-cache 23 | x-request-id: 7a6ce42f728bac23a887f64468a41a8d 24 | x-runtime: 0.044010 25 | x-swiftype-frontend-datacenter: dal05 26 | x-swiftype-frontend-node: web02.dal05 27 | x-swiftype-edge-datacenter: dal05 28 | x-swiftype-edge-node: web02.dal05 29 | 30 | {"errors":["Could not find engine."]} 31 | -------------------------------------------------------------------------------- /fixtures/host-c5s2mj.api.swiftype.com-443/multiSearchErrors: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/multi_search 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: host-c5s2mj.api.swiftype.com 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"queries\":[{\"badField\":\"whatever\",\"query\":\"cat\"},{\"query\":\"grumpy\"}]} 9 | 10 | HTTP/1.1 400 Bad Request 11 | date: Mon, 28 Jan 2019 15:55:46 GMT 12 | content-type: application/json; charset=utf-8 13 | transfer-encoding: chunked 14 | connection: close 15 | status: 400 Bad Request 16 | x-frame-options: SAMEORIGIN 17 | x-xss-protection: 1; mode=block 18 | x-content-type-options: nosniff 19 | x-ratelimit-limit: 6000 20 | x-ratelimit-remaining: 5999 21 | vary: Origin 22 | cache-control: no-cache 23 | x-request-id: 9660acf321b86cb57033aa1447a84249 24 | x-runtime: 0.093594 25 | x-swiftype-frontend-datacenter: dal05 26 | x-swiftype-frontend-node: web02.dal05 27 | x-swiftype-edge-datacenter: dal05 28 | x-swiftype-edge-node: web02.dal05 29 | 30 | [{"errors":["Options contains invalid key: badField"]},{"errors":[]}] 31 | -------------------------------------------------------------------------------- /fixtures/localhost-3002/15815391173475749: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: localhost:3002 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"name\":\"new-meta-engine\",\"type\":\"meta\",\"source_engines\":[\"source-engine-1\",\"source-engine-2\"]} 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | x-app-search-version: 8.3.2 15 | content-type: application/json; charset=utf-8 16 | vary: Origin 17 | etag: W/"cdb1493c20f29d2310c3222758a6c07b" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: a644b984-1214-4b9c-992e-0382e4443bf6 20 | x-runtime: 0.534623 21 | connection: close 22 | transfer-encoding: chunked 23 | 24 | {"name":"new-meta-engine","type":"meta","source_engines":["source-engine-1","source-engine-2"]} 25 | -------------------------------------------------------------------------------- /fixtures/localhost-3002/15815978342879229: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/new-meta-engine/source_engines 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: localhost:3002 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [\"source-engine-3\"] 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | x-app-search-version: 8.3.2 15 | content-type: application/json; charset=utf-8 16 | vary: Origin 17 | etag: W/"3d866b390e18c1bf51503b81707dc011" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: 95a0ef7d-ea6a-48f7-ba28-826f62c3e4fc 20 | x-runtime: 0.355023 21 | connection: close 22 | transfer-encoding: chunked 23 | 24 | {"name":"new-meta-engine","type":"meta","source_engines":["source-engine-1","source-engine-2","source-engine-3"]} 25 | -------------------------------------------------------------------------------- /fixtures/localhost-3002/158159799829857287: -------------------------------------------------------------------------------- 1 | DELETE /api/as/v1/engines/new-meta-engine/source_engines 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: localhost:3002 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: [\"source-engine-3\"] 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | x-app-search-version: 8.3.2 15 | content-type: application/json; charset=utf-8 16 | vary: Origin 17 | etag: W/"cdb1493c20f29d2310c3222758a6c07b" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: a0cc4718-3a7d-4b60-9cd2-32684a582e96 20 | x-runtime: 0.305840 21 | connection: close 22 | transfer-encoding: chunked 23 | 24 | {"name":"new-meta-engine","type":"meta","source_engines":["source-engine-1","source-engine-2"]} 25 | -------------------------------------------------------------------------------- /fixtures/localhost-3002/get-schema: -------------------------------------------------------------------------------- 1 | GET /api/as/v1/engines/swiftype-api-example/schema 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: localhost:3002 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {} 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | x-app-search-version: 8.3.2 15 | content-type: application/json; charset=utf-8 16 | vary: Origin 17 | etag: W/"ac828da6380c08d10df2df85cf73c250" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: 952ae812-c1fc-4292-bff5-e17276f6ea10 20 | x-runtime: 0.042176 21 | connection: close 22 | server: Jetty(9.2.29.v20191105) 23 | 24 | {"location":"geolocation","created_date":"date","title":"text","url":"text","views":"number"} 25 | -------------------------------------------------------------------------------- /fixtures/localhost-3002/update-schema: -------------------------------------------------------------------------------- 1 | POST /api/as/v1/engines/swiftype-api-example/schema 2 | x-swiftype-client: elastic-app-search-node 3 | x-swiftype-client-version: 8.3.2 4 | content-type: application/json 5 | host: localhost:3002 6 | authorization: Bearer api-mu75psc5egt9ppzuycnc2mc3 7 | accept: application/json 8 | body: {\"views\":\"text\"} 9 | 10 | HTTP/1.1 200 OK 11 | x-frame-options: SAMEORIGIN 12 | x-xss-protection: 1; mode=block 13 | x-content-type-options: nosniff 14 | x-app-search-version: 8.3.2 15 | content-type: application/json; charset=utf-8 16 | vary: Origin 17 | etag: W/"5669e9ad4cde52b649ba90479fff9e1f" 18 | cache-control: max-age=0, private, must-revalidate 19 | x-request-id: f80545b5-9269-43e7-844d-5dae39c4c172 20 | x-runtime: 0.184414 21 | connection: close 22 | server: Jetty(9.2.29.v20191105) 23 | 24 | {"location":"geolocation","created_date":"date","title":"text","url":"text","views":"text"} 25 | -------------------------------------------------------------------------------- /lib/appSearch.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2019 Elastic and contributors 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | const Client = require('./client') 20 | const jwt = require('jsonwebtoken') 21 | 22 | const SIGNED_SEARCH_TOKEN_JWT_ALGORITHM = 'HS256' 23 | 24 | const DEFAULT_BASE_URL_FN = (accountHostKey) => { 25 | return `https://${accountHostKey}.api.swiftype.com/api/as/v1/` 26 | } 27 | 28 | /** 29 | * Takes a paging object in the format {current, page} and converts it to an array of query string parameters, like 30 | * ['page[current]=1', 'page[size]=10'] 31 | */ 32 | function buildPagingParams(options) { 33 | return Object.keys(options).map(key => { 34 | const value = options[key] 35 | if (!value) return null 36 | return `page[${key}]=${value}` 37 | }).filter(v => v) 38 | } 39 | 40 | class AppSearchClient { 41 | constructor(accountHostKey, apiKey, baseUrlFn = DEFAULT_BASE_URL_FN) { 42 | const baseUrl = baseUrlFn(accountHostKey) 43 | this.client = new Client(apiKey, baseUrl) 44 | } 45 | 46 | /** 47 | * Send a search request to the App Search Api 48 | * https://swiftype.com/documentation/app-search/api/overview 49 | * 50 | * @param {String} engineName unique Engine name 51 | * @param {String} query String that is used to perform a search request. 52 | * @param {Object} options Object used for configuring the search like search_fields and result_fields 53 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 54 | */ 55 | search(engineName, query, options = {}) { 56 | options = Object.assign({}, options, { query }) 57 | return this.client.get(`engines/${encodeURIComponent(engineName)}/search`, options) 58 | } 59 | 60 | /** 61 | * Run multiple searches for documents on a single request 62 | * 63 | * @param {String} engineName unique Engine name 64 | * @param {Array} searches Searches to execute, [{query: String, options: Object}] 65 | * @returns {Promise} a Promise that returns an array of results {Object} when resolved, otherwise throws an Error. 66 | */ 67 | multiSearch(engineName, searches) { 68 | searches = searches.map(search => { 69 | return Object.assign({}, search.options, { query: search.query }) 70 | }) 71 | 72 | return this.client.post(`engines/${encodeURIComponent(engineName)}/multi_search`, { 73 | queries: searches 74 | }) 75 | } 76 | 77 | /** 78 | * Sends a query suggestion request to the App Search Api 79 | * 80 | * @param {String} engineName unique Engine name 81 | * @param {String} query String that is used to perform a query suggestion request. 82 | * @param {Object} options Object used for configuring the request 83 | */ 84 | querySuggestion(engineName, query, options = {}) { 85 | options = Object.assign({}, options, { query }) 86 | return this.client.post(`engines/${encodeURIComponent(engineName)}/query_suggestion`, options) 87 | } 88 | 89 | /** 90 | * Index a document. 91 | * 92 | * @param {String} engineName unique Engine name 93 | * @param {Object} document document object to be indexed. 94 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 95 | */ 96 | indexDocument(engineName, document) { 97 | return this.indexDocuments(engineName, [document]) 98 | .then((processedDocumentResults) => { 99 | return new Promise((resolve, reject) => { 100 | 101 | const processedDocumentResult = processedDocumentResults[0] 102 | const errors = processedDocumentResult['errors'] 103 | if (errors.length) { 104 | reject(new Error(errors.join('; '))) 105 | } 106 | delete processedDocumentResult['errors'] 107 | resolve(processedDocumentResult) 108 | }) 109 | }) 110 | } 111 | 112 | /** 113 | * Index a batch of documents. 114 | * 115 | * @param {String} engineName unique Engine name 116 | * @param {Array} documents Array of document objects to be indexed. 117 | * @returns {Promise>} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 118 | */ 119 | indexDocuments(engineName, documents) { 120 | return this.client.post(`engines/${encodeURIComponent(engineName)}/documents`, documents) 121 | } 122 | 123 | /** 124 | * Partial update a batch of documents. 125 | * 126 | * @param {String} engineName unique Engine name 127 | * @param {Array} documents Array of document objects to be updated. 128 | * @returns {Promise>} a Promise that returns an array of status objects, otherwise throws an Error. 129 | */ 130 | updateDocuments(engineName, documents) { 131 | return this.client.patch(`engines/${encodeURIComponent(engineName)}/documents`, documents) 132 | } 133 | 134 | /** 135 | * List all documents 136 | * 137 | * @param {String} engineName unique Engine name 138 | * @param {Object} options see the App Search API for supported search options 139 | * @returns {Promise>} a Promise that returns an array of documents, otherwise throws an Error. 140 | */ 141 | listDocuments(engineName, options = {}) { 142 | const pagingParams = buildPagingParams(options.page || {}) 143 | const prefix = pagingParams.length ? '?' : '' 144 | 145 | return this.client.get(`engines/${encodeURIComponent(engineName)}/documents/list${prefix}${pagingParams.join('&')}`, {}) 146 | } 147 | 148 | /** 149 | * Retrieve a batch of documents. 150 | * 151 | * @param {String} engineName unique Engine name 152 | * @param {Array} ids Array of document ids to be retrieved 153 | * @returns {Promise>} a Promise that returns an array of documents, otherwise throws an Error. 154 | */ 155 | getDocuments(engineName, ids) { 156 | return this.client.get(`engines/${encodeURIComponent(engineName)}/documents`, ids) 157 | } 158 | 159 | /** 160 | * Destroy a batch of documents. 161 | * 162 | * @param {String} engineName unique Engine name 163 | * @param {Array} ids Array of document ids to be destroyed 164 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. Includes "result" keys to maintain backward compatibility. 165 | */ 166 | destroyDocuments(engineName, ids) { 167 | return this.client.delete(`engines/${encodeURIComponent(engineName)}/documents`, ids) 168 | .then((response) => { 169 | response.forEach((docResult)=>{ 170 | docResult.result = docResult.deleted 171 | }) 172 | return response 173 | }) 174 | } 175 | 176 | 177 | /** 178 | * List all engines 179 | * 180 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 181 | */ 182 | listEngines(options = {}) { 183 | const pagingParams = buildPagingParams(options.page || {}) 184 | const prefix = pagingParams.length ? '?' : '' 185 | 186 | return this.client.get(`engines${prefix}${pagingParams.join('&')}`, {}) 187 | } 188 | 189 | /** 190 | * Retrieve an engine by name 191 | * 192 | * @param {String} engineName unique Engine name 193 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 194 | */ 195 | getEngine(engineName) { 196 | return this.client.get(`engines/${encodeURIComponent(engineName)}`, {}) 197 | } 198 | 199 | /** 200 | * Create a new engine 201 | * 202 | * @param {String} engineName unique Engine name 203 | * @param {Object} options 204 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 205 | */ 206 | createEngine(engineName, options) { 207 | return this.client.post(`engines`, { 208 | name: engineName, 209 | ...options 210 | }) 211 | } 212 | 213 | /** 214 | * Add a Source Engine to a Meta Engine 215 | * 216 | * @param {String} engineName Name of Meta Engine 217 | * @param {Array[String]} sourceEngines Names of Engines to use as Source Engines 218 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 219 | */ 220 | addMetaEngineSources(engineName, sourceEngines) { 221 | return this.client.post(`engines/${engineName}/source_engines`, sourceEngines) 222 | } 223 | 224 | /** 225 | * Remove a Source Engine from a Meta Engine 226 | * 227 | * @param {String} engineName Name of Meta Engine 228 | * @param {Array[String]} sourceEngines Names of existing Source Engines to remove 229 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 230 | */ 231 | deleteMetaEngineSources(engineName, sourceEngines) { 232 | return this.client.delete(`engines/${engineName}/source_engines`, sourceEngines) 233 | } 234 | 235 | /** 236 | * Delete an engine 237 | * 238 | * @param {String} engineName unique Engine name 239 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 240 | */ 241 | destroyEngine(engineName) { 242 | return this.client.delete(`engines/${encodeURIComponent(engineName)}`, {}) 243 | } 244 | 245 | 246 | /** 247 | * List all Curations 248 | * 249 | * @param {String} engineName unique Engine name 250 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 251 | */ 252 | listCurations(engineName, options = {}) { 253 | const pagingParams = buildPagingParams(options.page || {}); 254 | const prefix = pagingParams.length ? '?' : ''; 255 | 256 | return this.client.get(`engines/${encodeURIComponent(engineName)}/curations${prefix}${pagingParams.join('&')}`, {}) 257 | } 258 | 259 | /** 260 | * Retrieve a Curation by id 261 | * 262 | * @param {String} engineName unique Engine name 263 | * @param {String} curationId unique Curation id 264 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 265 | */ 266 | getCuration(engineName, curationId) { 267 | 268 | return this.client.get(`engines/${encodeURIComponent(engineName)}/curations/${encodeURIComponent(curationId)}`, {}) 269 | } 270 | 271 | /** 272 | * Create a new Curation 273 | * 274 | * @param {String} engineName unique Engine name 275 | * @param {Object} newCuration body of the Curation object 276 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 277 | */ 278 | createCuration(engineName, newCuration) { 279 | return this.client.post(`engines/${encodeURIComponent(engineName)}/curations`, newCuration) 280 | } 281 | 282 | /** 283 | * Update an existing curation 284 | * 285 | * @param {String} engineName unique Engine name 286 | * @param {String} curationId unique Curation id 287 | * @param {Object} newCuration body of the Curation object 288 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 289 | */ 290 | updateCuration(engineName, curationId, newCuration) { 291 | return this.client.put(`engines/${encodeURIComponent(engineName)}/curations/${encodeURIComponent(curationId)}`, newCuration) 292 | } 293 | 294 | /** 295 | * Create a new meta engine 296 | * 297 | * @param {String} engineName unique Engine name 298 | * @param {Array[String]} sourceEngines list of engine names to use as source engines 299 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 300 | */ 301 | createMetaEngine(engineName, sourceEngines) { 302 | return this.client.post(`engines`, { 303 | name: engineName, 304 | type: 'meta', 305 | source_engines: sourceEngines 306 | }) 307 | } 308 | 309 | /** 310 | * Delete a curation 311 | * 312 | * @param {String} engineName unique Engine name 313 | * @param {String} curationId unique Curation name 314 | * @returns {Promise} a Promise that returns a result {Object} when resolved, otherwise throws an Error. 315 | */ 316 | destroyCuration(engineName, curationId) { 317 | return this.client.delete(`engines/${encodeURIComponent(engineName)}/curations/${encodeURIComponent(curationId)}`, {}) 318 | } 319 | 320 | /** 321 | * Retrieve a schema by engine name 322 | * 323 | * @param {String} engineName unique Engine name 324 | * @returns {Promise} a Promise that returns the current schema {Object} when resolved, otherwise throws an Error. 325 | */ 326 | getSchema(engineName) { 327 | return this.client.get(`engines/${encodeURIComponent(engineName)}/schema`, {}) 328 | } 329 | 330 | /** 331 | * Update an existing schema 332 | * 333 | * @param {String} engineName unique Engine name 334 | * @param {Object} schema body of schema object 335 | * @returns {Promise} a Promise that returns the current schema {Object} when resolved, otherwise throws an Error. 336 | */ 337 | updateSchema(engineName, schema) { 338 | return this.client.post(`engines/${encodeURIComponent(engineName)}/schema`, schema) 339 | } 340 | 341 | /** 342 | * Creates a jwt search key that can be used for authentication to enforce a set of required search options. 343 | * 344 | * @param {String} apiKey the API Key used to sign the search key 345 | * @param {String} apiKeyName the unique name for the API Key 346 | * @param {Object} options Object see the App Search API for supported search options 347 | * @param {Object} signOptions Object see the node-jsonwebtoken for supported sign options 348 | * @returns {String} jwt search key 349 | */ 350 | static createSignedSearchKey(apiKey, apiKeyName, options = {}, signOptions = {}) { 351 | const payload = Object.assign({}, options, { api_key_name: apiKeyName }) 352 | return jwt.sign(payload, apiKey, Object.assign({}, signOptions, { algorithm: SIGNED_SEARCH_TOKEN_JWT_ALGORITHM })) 353 | } 354 | } 355 | 356 | module.exports = AppSearchClient 357 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2019 Elastic and contributors 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | const packageJson = require('../package.json') 20 | const request = require('request') 21 | 22 | function getErrorMessages(responseBody) { 23 | // Some responses, like a 404 return an error array. 24 | if (responseBody.errors) { 25 | return responseBody.errors 26 | } 27 | 28 | // Other responses, like an Auth error, return a single 29 | // error. 30 | if (responseBody.error) { 31 | return [responseBody.error] 32 | } 33 | 34 | // Multi-search returns an array of responses, with error arrays. 35 | if (Array.isArray(responseBody)) { 36 | return responseBody.reduce((acc, response) => { 37 | const errors = (response && Array.isArray(response.errors)) 38 | ? response.errors 39 | : [] 40 | return [...acc, ...errors] 41 | }, []) 42 | } 43 | } 44 | class Client { 45 | constructor(apiKey, baseUrl) { 46 | this.apiKey = apiKey 47 | this.clientName = 'elastic-app-search-node' 48 | this.clientVersion = packageJson.version 49 | this.baseUrl = baseUrl 50 | } 51 | 52 | get(path, params) { 53 | return this._jsonRequest('GET', path, params) 54 | } 55 | 56 | post(path, params) { 57 | return this._jsonRequest('POST', path, params) 58 | } 59 | 60 | put(path, params) { 61 | return this._jsonRequest('PUT', path, params) 62 | } 63 | 64 | patch(path, params) { 65 | return this._jsonRequest('PATCH', path, params) 66 | } 67 | 68 | delete(path, params) { 69 | return this._jsonRequest('DELETE', path, params) 70 | } 71 | 72 | _jsonRequest(method, path, params) { 73 | return this._wrap({ 74 | method: method, 75 | url: `${this.baseUrl}${path}`, 76 | json: params, 77 | auth: { 78 | bearer: this.apiKey 79 | }, 80 | headers: { 81 | 'X-Swiftype-Client': this.clientName, 82 | 'X-Swiftype-Client-Version': this.clientVersion, 83 | 'Content-Type': 'application/json' 84 | } 85 | }) 86 | } 87 | 88 | _wrap(options) { 89 | return new Promise((resolve, reject) => { 90 | request(options, (error, response, body) => { 91 | if (error) { 92 | error.errorMessages = [error.message] 93 | reject(error) 94 | } else if (!(response.statusCode.toString().match(/2[\d]{2}/))) { 95 | var apiError = new Error(response.statusMessage) 96 | apiError.errorMessages = getErrorMessages(response.body) 97 | reject(apiError) 98 | } else { 99 | resolve(body) 100 | } 101 | }) 102 | }) 103 | } 104 | } 105 | 106 | module.exports = Client 107 | -------------------------------------------------------------------------------- /logo-app-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/app-search-node/7c8f794a7ed212688dd3dc870c512d0ec8cc06c8/logo-app-search.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/app-search-node", 3 | "version": "8.3.2", 4 | "description": "Elastic App Search Node.js client", 5 | "files": [ 6 | "lib/" 7 | ], 8 | "main": "lib/appSearch.js", 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/elastic/app-search-node.git" 15 | }, 16 | "engines": { 17 | "node": ">= v6.5.0" 18 | }, 19 | "author": "Elastic", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/elastic/app-search-node/issues" 23 | }, 24 | "homepage": "https://github.com/elastic/app-search-node#readme", 25 | "devDependencies": { 26 | "mocha": "^7.0.0", 27 | "replay": "~2.4.0" 28 | }, 29 | "dependencies": { 30 | "request": "^2.81.0", 31 | "jsonwebtoken": "^8.3.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2019 Elastic and contributors 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | const assert = require('assert') 20 | const AppSearchClient = require('../lib/appSearch') 21 | const replay = require('replay') 22 | 23 | describe('AppSearchClient', () => { 24 | const hostIdentifier = process.env.HOST_IDENTIFIER || 'host-c5s2mj' 25 | const apiKey = process.env.API_KEY || 'api-mu75psc5egt9ppzuycnc2mc3' 26 | const client = new AppSearchClient(hostIdentifier, apiKey) 27 | 28 | replay.reset('localhost') // So that replay can capture localhost requests 29 | const baseUrlFn = () => "http://localhost:3002/api/as/v1/"; 30 | const selfManagedClient = new AppSearchClient(undefined, apiKey, baseUrlFn) 31 | 32 | const engineName = 'swiftype-api-example' 33 | const documents = [ 34 | { 35 | id: 'INscMGmhmX4', 36 | url: 'http://www.youtube.com/watch?v=v1uyQZNg2vE', 37 | title: 'The Original Grumpy Cat', 38 | body: 'this is a test' 39 | }, 40 | { 41 | id: 'JNDFojsd02', 42 | url: 'http://www.youtube.com/watch?v=tsdfhk2j', 43 | title: 'Another Grumpy Cat', 44 | body: 'this is also a test' 45 | } 46 | ] 47 | const curations = [ 48 | { 49 | "id": "cur-9382", 50 | "queries": [ 51 | "mew hiss" 52 | ], 53 | "promoted": [ 54 | "INscMGmhmX4" 55 | ], 56 | "hidden": [ 57 | "JNDFojsd02" 58 | ] 59 | }, 60 | { 61 | "id": "cur-378291", 62 | "queries": [ 63 | "grrr scratch" 64 | ], 65 | "promoted": [ 66 | "JNDFojsd02" 67 | ], 68 | "hidden": [ 69 | "INscMGmhmX4" 70 | ] 71 | } 72 | ] 73 | 74 | describe('#indexDocument', () => { 75 | it('should index a document successfully', (done) => { 76 | client.indexDocument(engineName, documents[0]) 77 | .then((result) => { 78 | assert.deepEqual({ 'id': 'INscMGmhmX4' }, result) 79 | done() 80 | }) 81 | .catch((error) => { 82 | done(error) 83 | }) 84 | }) 85 | }) 86 | 87 | describe('#indexDocuments', () => { 88 | it('should index documents successfully', (done) => { 89 | client.indexDocuments(engineName, documents) 90 | .then((results) => { 91 | assert.deepEqual([ 92 | { 'errors': [], 'id': 'INscMGmhmX4' }, 93 | { 'errors': [], 'id': 'JNDFojsd02' } 94 | ], results) 95 | done() 96 | }) 97 | .catch((error) => { 98 | done(error) 99 | }) 100 | }) 101 | }) 102 | 103 | describe('#updateDocuments', () => { 104 | it('should update documents successfully', (done) => { 105 | client.updateDocuments(engineName, documents) 106 | .then((results) => { 107 | assert.deepEqual([ 108 | { 'errors': [], 'id': 'INscMGmhmX4' }, 109 | { 'errors': [], 'id': 'JNDFojsd02' } 110 | ], results) 111 | done() 112 | }) 113 | .catch((error) => { 114 | done(error) 115 | }) 116 | }) 117 | }) 118 | 119 | describe('#getDocuments', () => { 120 | const documentIds = documents.map((d) => d.id) 121 | 122 | it('should get documents successfully', (done) => { 123 | client.getDocuments(engineName, documentIds) 124 | .then((results) => { 125 | assert.deepEqual([ 126 | { 127 | 'body': 'this is a test', 128 | 'id': 'INscMGmhmX4', 129 | 'title': 'The Original Grumpy Cat', 130 | 'url': 'http://www.youtube.com/watch?v=v1uyQZNg2vE', 131 | }, 132 | { 133 | 'body': 'this is also a test', 134 | 'id': 'JNDFojsd02', 135 | 'title': 'Another Grumpy Cat', 136 | 'url': 'http://www.youtube.com/watch?v=tsdfhk2j', 137 | } 138 | ], results) 139 | done() 140 | }) 141 | .catch((error) => { 142 | done(error) 143 | }) 144 | }) 145 | }) 146 | 147 | describe('#listDocuments', () => { 148 | it('should list documents successfully', (done) => { 149 | client.listDocuments(engineName) 150 | .then((results) => { 151 | assert.deepEqual([ 152 | { 153 | 'body': 'this is a test', 154 | 'id': 'INscMGmhmX4', 155 | 'title': 'The Original Grumpy Cat', 156 | 'url': 'http://www.youtube.com/watch?v=v1uyQZNg2vE', 157 | }, 158 | { 159 | 'body': 'this is also a test', 160 | 'id': 'JNDFojsd02', 161 | 'title': 'Another Grumpy Cat', 162 | 'url': 'http://www.youtube.com/watch?v=tsdfhk2j', 163 | } 164 | ], results.results) 165 | done() 166 | }) 167 | .catch((error) => { 168 | done(error) 169 | }) 170 | }) 171 | }) 172 | 173 | describe('#destroyDocuments', () => { 174 | it('should destroy documents', (done) => { 175 | client.destroyDocuments(engineName, ['INscMGmhmX4', 'FakeId']) 176 | .then((results) => { 177 | assert.deepEqual([ 178 | { 'id': 'INscMGmhmX4', 'deleted': true, 'result': true }, 179 | { 'id': 'FakeId', 'deleted': false, 'result': false }, 180 | ], results) 181 | done() 182 | }) 183 | .catch((error) => { 184 | done(error) 185 | }) 186 | }) 187 | }) 188 | 189 | describe('#listEngines', () => { 190 | it('should list engines successfully', (done) => { 191 | client.listEngines() 192 | .then((results) => { 193 | assert.deepEqual({ 194 | 'meta': { 195 | 'page': { 196 | 'current': 1, 197 | 'total_pages': 1, 198 | 'total_results': 3, 199 | 'size': 25 200 | } 201 | }, 202 | 'results': [{ 203 | 'name': 'node-modules', 204 | 'type': 'default', 205 | 'language': null 206 | }, { 207 | 'name': 'ruby-gems', 208 | 'type': 'default', 209 | 'language': null 210 | }, { 211 | 'name': 'test-engine', 212 | 'type': 'default', 213 | 'language': null 214 | }] 215 | }, results) 216 | done() 217 | }) 218 | .catch((error) => { 219 | done(error) 220 | }) 221 | }) 222 | 223 | it('should support paging', (done) => { 224 | client.listEngines({ 225 | page: { 226 | current: 2, 227 | size: 1 228 | } 229 | }) 230 | .then((results) => { 231 | assert.deepEqual({ 232 | 'meta': { 233 | 'page': { 234 | 'current': 2, 235 | 'total_pages': 3, 236 | 'total_results': 3, 237 | 'size': 1 238 | } 239 | }, 240 | 'results': [{ 241 | 'name': 'ruby-gems', 242 | 'type': 'default', 243 | 'language': null 244 | }] 245 | }, results) 246 | done() 247 | }) 248 | .catch((error) => { 249 | done(error) 250 | }) 251 | }) 252 | }) 253 | 254 | describe('#getEngine', () => { 255 | it('should get an engine successfully', (done) => { 256 | client.getEngine(engineName) 257 | .then((results) => { 258 | assert.deepEqual({ 259 | 'name': 'swiftype-api-example', 260 | 'type': 'default', 261 | 'language': null 262 | }, results) 263 | done() 264 | }) 265 | .catch((error) => { 266 | done(error) 267 | }) 268 | }) 269 | }) 270 | 271 | describe('#createEngine', () => { 272 | it('should create an engine successfully', (done) => { 273 | client.createEngine('new-engine') 274 | .then((results) => { 275 | assert.deepEqual({ 276 | 'name': 'new-engine', 277 | 'type': 'default', 278 | 'language': null 279 | }, results) 280 | done() 281 | }) 282 | .catch((error) => { 283 | done(error) 284 | }) 285 | }) 286 | 287 | it('should create an engine with a language', (done) => { 288 | client.createEngine('new-engine', {language: 'en'}) 289 | .then((results) => { 290 | assert.deepEqual({ 291 | 'name': 'new-engine', 292 | 'type': 'default', 293 | 'language': 'en' 294 | }, results) 295 | done() 296 | }) 297 | .catch((error) => { 298 | done(error) 299 | }) 300 | }) 301 | }) 302 | 303 | describe('#destroyEngine', () => { 304 | it('should delete an engine successfully', (done) => { 305 | client.destroyEngine('new-engine') 306 | .then((results) => { 307 | assert.deepEqual({ 308 | 'deleted': true 309 | }, results) 310 | done() 311 | }) 312 | .catch((error) => { 313 | done(error) 314 | }) 315 | }) 316 | }) 317 | 318 | describe('#createMetaEngine', () => { 319 | it('should create a meta engine', (done) => { 320 | selfManagedClient.createMetaEngine('new-meta-engine', ['source-engine-1', 'source-engine-2']) 321 | .then((results) => { 322 | assert.deepEqual({ 323 | 'name': 'new-meta-engine', 324 | 'type': 'meta', 325 | 'source_engines': [ 326 | 'source-engine-1', 327 | 'source-engine-2' 328 | ] 329 | }, results) 330 | done() 331 | }) 332 | .catch((error) => { 333 | done(error) 334 | }) 335 | }) 336 | }) 337 | 338 | describe('#addMetaEngineSources', () => { 339 | it('should add a Source Engine to a Meta Engine', (done) => { 340 | selfManagedClient.addMetaEngineSources('new-meta-engine', ['source-engine-3']) 341 | .then((results) => { 342 | assert.deepEqual({ 343 | 'name': 'new-meta-engine', 344 | 'type': 'meta', 345 | 'source_engines': [ 346 | 'source-engine-1', 347 | 'source-engine-2', 348 | 'source-engine-3' 349 | ] 350 | }, results) 351 | done() 352 | }) 353 | .catch((error) => { 354 | done(error) 355 | }) 356 | }) 357 | }) 358 | 359 | describe('#deleteMetaEngineSources', () => { 360 | it('Remove a Source Engine from a Meta Engine', (done) => { 361 | selfManagedClient.deleteMetaEngineSources('new-meta-engine', ['source-engine-3']) 362 | .then((results) => { 363 | assert.deepEqual({ 364 | 'name': 'new-meta-engine', 365 | 'type': 'meta', 366 | 'source_engines': [ 367 | 'source-engine-1', 368 | 'source-engine-2' 369 | ] 370 | }, results) 371 | done() 372 | }) 373 | .catch((error) => { 374 | done(error) 375 | }) 376 | }) 377 | }) 378 | 379 | describe('#search', () => { 380 | it('should query', (done) => { 381 | client.search(engineName, 'cat') 382 | .then((resp) => { 383 | assert.deepEqual([ 384 | { raw: 'The Original Grumpy Cat' }, 385 | { raw: 'Another Grumpy Cat' } 386 | ], resp.results.map((r) => r.title )) 387 | done() 388 | }) 389 | .catch((error) => { 390 | done(error) 391 | }) 392 | }) 393 | }) 394 | 395 | describe('#multiSearch', () => { 396 | it('should query', (done) => { 397 | client.multiSearch(engineName, [{ 398 | query: 'cat', 399 | options: {} 400 | }, { 401 | query: 'grumpy', 402 | options: {} 403 | }]) 404 | .then((resp) => { 405 | assert.deepEqual(resp.map(res => res.results.map(res => res.title.raw)), [[ 406 | 'The Original Grumpy Cat', 407 | 'Another Grumpy Cat' 408 | ], [ 409 | 'Another Grumpy Cat', 410 | 'The Original Grumpy Cat' 411 | ] 412 | ]) 413 | done() 414 | }) 415 | .catch((error) => { 416 | done(error) 417 | }) 418 | }) 419 | }) 420 | 421 | describe('#querySuggestion', () => { 422 | it('should request query suggestions', (done) => { 423 | client.querySuggestion(engineName, 'cat') 424 | .then((resp) => { 425 | assert.deepEqual({ 426 | results: { documents: [{ suggestion: "cat" }] }, 427 | meta: { request_id: "7414beb06c644b1aa88accb6019c6d6f" } 428 | }, resp) 429 | done() 430 | }) 431 | .catch((error) => { 432 | done(error) 433 | }) 434 | }) 435 | 436 | it('should request query suggestions with options', (done) => { 437 | client.querySuggestion(engineName, 'cat', { 438 | size: 3, 439 | types: { 440 | documents: { 441 | fields: ['title'] 442 | } 443 | } 444 | }) 445 | .then((resp) => { 446 | assert.deepEqual({ 447 | results: { documents: [{ suggestion: "cat" }] }, 448 | meta: { request_id: "a0e24c46d4379e58bf871a6a515b8d94" } 449 | }, resp) 450 | done() 451 | }) 452 | .catch((error) => { 453 | done(error) 454 | }) 455 | }) 456 | }) 457 | 458 | describe('#createSignedSearchKey', () => { 459 | it('should build a valid jwt', (done) => { 460 | token = AppSearchClient.createSignedSearchKey('private-mu75psc5egt9ppzuycnc2mc3', 'my-token-name', { query: 'cat' }) 461 | jwt = require('jsonwebtoken') 462 | decoded = jwt.verify(token, 'private-mu75psc5egt9ppzuycnc2mc3') 463 | assert.equal(decoded.api_key_name, 'my-token-name') 464 | assert.equal(decoded.query, 'cat') 465 | done() 466 | }) 467 | it('should pass sign options to node-jsonwebtoken', (done) => { 468 | token = AppSearchClient.createSignedSearchKey('private-mu75psc5egt9ppzuycnc2mc3', 'my-token-name', { query: 'cat' }, { expiresIn: '5 minutes' }) 469 | jwt = require('jsonwebtoken') 470 | decoded = jwt.verify(token, 'private-mu75psc5egt9ppzuycnc2mc3') 471 | assert.ok(decoded.exp) 472 | done() 473 | }) 474 | }) 475 | 476 | describe('#listCurations', () => { 477 | it('should list curations successfully', (done) => { 478 | client.listCurations(engineName) 479 | .then((results) => { 480 | assert.deepEqual({ 481 | "meta": { 482 | "page": { 483 | "current": 1, 484 | "total_pages": 1, 485 | "total_results": 2, 486 | "size": 25 487 | } 488 | }, 489 | "results": curations 490 | }, results) 491 | done() 492 | }) 493 | .catch((error) => { 494 | done(error) 495 | }) 496 | }) 497 | 498 | it('should support paging', (done) => { 499 | client.listCurations(engineName, { 500 | page: { 501 | current: 2, 502 | size: 1 503 | } 504 | }) 505 | .then((results) => { 506 | assert.deepEqual({ 507 | "meta": { 508 | "page": { 509 | "current": 2, 510 | "total_pages": 2, 511 | "total_results": 2, 512 | "size": 1 513 | } 514 | }, 515 | "results": [ 516 | curations[1] 517 | ] 518 | }, results) 519 | done() 520 | }) 521 | .catch((error) => { 522 | done(error) 523 | }) 524 | }) 525 | }) 526 | 527 | describe('#getCuration', () => { 528 | it('should get a curation successfully', (done) => { 529 | const expected = { 530 | meta: { 531 | page: { current: 1, total_pages: 1, total_results: 1, size: 25 } 532 | }, 533 | results: 534 | [ curations[0] ] 535 | } 536 | client.getCuration(engineName, curations[0].id) 537 | .then((results) => { 538 | assert.deepEqual(expected, results) 539 | done() 540 | }) 541 | .catch((error) => { 542 | done(error) 543 | }) 544 | }) 545 | 546 | it('passes along errors', (done) => { 547 | client.getCuration(engineName, "cur-0000") 548 | .then((results) => { 549 | done(new Error('This should have resulted in an error')) 550 | }) 551 | .catch((error) => { 552 | assert(error.errorMessages.join().toString() === "Curation not found with id: cur-0000") 553 | done() 554 | }) 555 | }) 556 | }) 557 | 558 | describe('#createCuration', () => { 559 | it('should create a curation successfully', (done) => { 560 | client.createCuration(engineName, {queries: ["arf arf"], promoted: [], hidden: ["INscMGmhmX4", "JNDFojsd02"]}) 561 | .then((results) => { 562 | assert.deepEqual({ 563 | "id": "cur-9043829" 564 | }, results) 565 | done() 566 | }) 567 | .catch((error) => { 568 | done(error) 569 | }) 570 | }) 571 | }) 572 | 573 | describe('#destroyCuration', () => { 574 | it('should delete a curation successfully', (done) => { 575 | client.destroyCuration(engineName, curations[1].id) 576 | .then((results) => { 577 | assert.deepEqual({ 578 | "deleted": true 579 | }, results) 580 | done() 581 | }) 582 | .catch((error) => { 583 | done(error) 584 | }) 585 | }) 586 | }) 587 | 588 | describe('#updateCuration', () => { 589 | it('should update a curation successfully', (done) => { 590 | client.updateCuration(engineName, "cur-9382", {queries: ["mew hiss", "sideways hop"], promoted: ["INscMGmhmX4"], hidden: ["JNDFojsd02"]}) 591 | .then((results) => { 592 | assert.deepEqual({ 593 | "id": "cur-9382" 594 | }, results) 595 | done() 596 | }) 597 | .catch((error) => { 598 | done(error) 599 | }) 600 | }) 601 | }) 602 | 603 | describe('#getSchema', () => { 604 | it('should get a schema successfully', (done) => { 605 | selfManagedClient.getSchema(engineName) 606 | .then((result) => { 607 | assert.deepEqual({ 608 | 'url': 'text', 609 | 'title': 'text', 610 | 'views': 'number', 611 | 'location': 'geolocation', 612 | 'created_date': 'date' 613 | }, result) 614 | done() 615 | }) 616 | .catch((error) => { 617 | done(error) 618 | }) 619 | }) 620 | }) 621 | 622 | describe('#updateSchema', () => { 623 | it('should update a schema successfully', (done) => { 624 | selfManagedClient.updateSchema(engineName, { 'views': 'text' }) 625 | .then((result) => { 626 | assert.deepEqual({ 627 | 'url': 'text', 628 | 'title': 'text', 629 | 'views': 'text', 630 | 'location': 'geolocation', 631 | 'created_date': 'date' 632 | }, result) 633 | done() 634 | }) 635 | .catch((error) => { 636 | done(error) 637 | }) 638 | }) 639 | }) 640 | 641 | describe('error handling', () => { 642 | it('should handle 404', (done) => { 643 | client.search('invalid-engine-name', 'cat') 644 | .then(() => { 645 | done(new Error('was expected to throw with an error message')) 646 | }) 647 | .catch(error => { 648 | const errorMessages = (error && error.errorMessages) ? error.errorMessages : null 649 | 650 | if (errorMessages) { 651 | try { 652 | assert.deepEqual(errorMessages, ['Could not find engine.']) 653 | done() 654 | } catch (e) { 655 | done(e) 656 | } 657 | } else { 658 | done(error) 659 | } 660 | }) 661 | }) 662 | 663 | it('should handle auth error', (done) => { 664 | const badAuthClient = new AppSearchClient(hostIdentifier, 'invalid') 665 | badAuthClient.search(engineName, 'cat') 666 | .then(() => { 667 | done(new Error('was expected to throw with an error message')) 668 | }) 669 | .catch(error => { 670 | const errorMessages = (error && error.errorMessages) ? error.errorMessages : null 671 | 672 | if (errorMessages) { 673 | try { 674 | assert.deepEqual(errorMessages, ['Invalid authentication token.']) 675 | done() 676 | } catch (e) { 677 | done(e) 678 | } 679 | } else { 680 | done(error) 681 | } 682 | }) 683 | }) 684 | 685 | it('should handle multi_search errors', (done) => { 686 | client.multiSearch(engineName, [ 687 | { 688 | query: 'cat', 689 | options: { 690 | badField: 'whatever' 691 | } 692 | }, { 693 | query: 'grumpy', 694 | options: {} 695 | } 696 | ]) 697 | .then(() => { 698 | done(new Error('was expected to throw with an error message')) 699 | }) 700 | .catch(error => { 701 | const errorMessages = (error && error.errorMessages) ? error.errorMessages : null 702 | 703 | if (errorMessages) { 704 | try { 705 | assert.deepEqual(errorMessages, ['Options contains invalid key: badField']) 706 | done() 707 | } catch (e) { 708 | done(e) 709 | } 710 | } else { 711 | done(error) 712 | } 713 | }) 714 | }) 715 | }) 716 | }) 717 | --------------------------------------------------------------------------------