├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── index.php └── src ├── SearchResult.php ├── TypesenseClient.php ├── TypesenseCommand.php ├── TypesenseConfig.php ├── TypesenseDocument.php ├── TypesenseException.php ├── TypesenseItem.php ├── TypesenseItemInterface.php └── TypesenseSearch.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.php] 13 | indent_size = 4 14 | 15 | [*.md,*.txt] 16 | trim_trailing_whitespace = false 17 | insert_final_newline = false 18 | 19 | [composer.json] 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Note: You need to uncomment the lines you want to use; the other lines can be deleted 2 | 3 | # Git 4 | # .gitattributes export-ignore 5 | # .gitignore export-ignore 6 | 7 | # Tests 8 | # /.coveralls.yml export-ignore 9 | # /.travis.yml export-ignore 10 | # /phpunit.xml.dist export-ignore 11 | # /tests/ export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS files 2 | .DS_Store 3 | 4 | # npm modules 5 | /node_modules 6 | 7 | # files of Composer dependencies that are not needed for the plugin 8 | vendor 9 | /vendor/**/.* 10 | /vendor/**/*.json 11 | /vendor/**/*.txt 12 | /vendor/**/*.md 13 | /vendor/**/*.yml 14 | /vendor/**/*.yaml 15 | /vendor/**/*.xml 16 | /vendor/**/*.dist 17 | /vendor/**/readme.php 18 | /vendor/**/LICENSE 19 | /vendor/**/COPYING 20 | /vendor/**/VERSION 21 | /vendor/**/docs/* 22 | /vendor/**/example/* 23 | /vendor/**/examples/* 24 | /vendor/**/test/* 25 | /vendor/**/tests/* 26 | /vendor/**/php4/* 27 | /vendor/getkirby/composer-installer 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maxime CHÊNE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kirby Typesense 2 | 3 | This plugin allow you to index kirby pages into typesense and make fulltext search available for your website. 4 | 5 | ## Overview 6 | 7 | > This plugin is free and under MIT license, I'm glad if you find it usefull. If you plan to use it for commercial use, 8 | > or if I saved you some time, 9 | > please consider helping me to maintain this by either [making a donation](https://www.paypal.com/paypalme/maximechene) 10 | > of your choice or [sponsoring me](https://github.com/sponsors/maxchene). 11 | 12 | ## 1. Install Typesense on your server 13 | 14 | ### 1.1 What is Typesense ? 15 | 16 | [Typesense](https://typesense.org) is an opensource alternative for Algolia or ElasticSearch. It is free and provide a 17 | fast typo tolerant search engine. 18 | 19 | ### 1.2 Install Typesense 20 | 21 | To use this plugin, you need 22 | to [install Typesense on your server](https://typesense.org/docs/guide/install-typesense.html). 23 | 24 |
25 | 26 | ## 2. Install the Kirby Typesense plugin 27 | 28 | Download and copy this repository to ```/site/plugins/typesense``` 29 | 30 | Or even better, to get easy updates, install it with composer: ```composer require maxchene/kirby3-typesense``` 31 | 32 |
33 | 34 | ## 3. Configuration 35 | 36 | Edit your config file: ```site/config/config.php``` to add your own plugin configuration. 37 | 38 | Here is what it should look like: 39 | 40 | ```php 41 | 'maxchene.typesense' => [ 42 | 'host' => 'localhost:8108', # typesense host and port 43 | 'key' => 'secret', # typesense API key 44 | 'num_typos' => 2, # number of allowed typo error 45 | 'schema' => [ 46 | 'name' => 'my-collection', 47 | 'fields' => [ 48 | ['name' => 'content', 'type' => 'string'], 49 | ['name' => 'type', 'type' => 'string'] 50 | ] 51 | ], 52 | 'templates' => [ 53 | 'article' => function (Page $page) { 54 | 55 | }, 56 | 'default' => function (Page $page) { 57 | return [ 58 | 'content' => 'text content that should be indexed for fulltext search', 59 | 'type' => 'default' 60 | ]; 61 | } 62 | ] 63 | ] 64 | ``` 65 | 66 |
67 | 68 | ### 3.1 Schema configuration 69 | 70 | ### 3.2 Templates configuration 71 | 72 | ## 4. Use fulltext search 73 | 74 | All fields provided in the config file will be searched. 75 | 76 | | Param | Description | Type | Optionnal | Default value | 77 | |--------|------------------------------------|---------|-----------|---------------| 78 | | $query | your search string | string | no | | 79 | | $limit | Maximum number of results per page | integer | yes | 30 | 80 | | $page | Page number | integer | yes | 1 | 81 | 82 |
83 | 84 | ### 4.1 Site method 85 | 86 | This plugins gives you access to the ```$site->typesenseSearch(string $query, int $limit, int $page)``` method 87 | wherever ```$site``` is available: templates, controllers,... 88 | 89 |
90 | 91 | ### 4.2 Typesense Search 92 | 93 | You can also create a TypesenseSearch instance : 94 | 95 | ````php 96 | $searchEngine = new TypesenseSearch(); 97 | $results = $searchEngine->search($query, $limit, $page); 98 | 99 | ```` 100 | 101 | ## 5. Command Line 102 | 103 | ## 6. Full configuration exemple 104 | 105 | ## 7. TODOS 106 | 107 | - add command line tools 108 | - improve docs or enable wiki pages for this repo 109 | - maybe add a Page method to build indexed content 110 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maxchene/kirby3-typesense", 3 | "homepage": "https://github.com/maxchene/kirby3-typesense", 4 | "description": "Kirby plugin that use typesense for quick search", 5 | "license": "MIT", 6 | "type": "kirby-plugin", 7 | "version": "1.0.2", 8 | "authors": [ 9 | { 10 | "name": "Maxime CHENE", 11 | "email": "maxime@kaliel.fr" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=8.1", 16 | "getkirby/composer-installer": "^1.1" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Maxchene\\Typesense\\": "src/" 21 | } 22 | }, 23 | "config": { 24 | "optimize-autoloader": true, 25 | "allow-plugins": { 26 | "getkirby/composer-installer": true 27 | } 28 | }, 29 | "extra": { 30 | "installer-name": "typesense" 31 | }, 32 | "keywords": [ 33 | "kirby", 34 | "kirby3", 35 | "kirby3-cms", 36 | "kirby3-plugin", 37 | "typesense", 38 | "fulltext", 39 | "search" 40 | ], 41 | "require-dev": { 42 | "symfony/var-dumper": "^6.2", 43 | "getkirby/cli": "^1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "438e42e92566ee7b88ce61aa948aabbc", 8 | "packages": [ 9 | { 10 | "name": "getkirby/composer-installer", 11 | "version": "1.2.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/getkirby/composer-installer.git", 15 | "reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/getkirby/composer-installer/zipball/c98ece30bfba45be7ce457e1102d1b169d922f3d", 20 | "reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "composer-plugin-api": "^1.0 || ^2.0" 25 | }, 26 | "require-dev": { 27 | "composer/composer": "^1.8 || ^2.0" 28 | }, 29 | "type": "composer-plugin", 30 | "extra": { 31 | "class": "Kirby\\ComposerInstaller\\Plugin" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Kirby\\": "src/" 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins", 43 | "homepage": "https://getkirby.com", 44 | "support": { 45 | "issues": "https://github.com/getkirby/composer-installer/issues", 46 | "source": "https://github.com/getkirby/composer-installer/tree/1.2.1" 47 | }, 48 | "funding": [ 49 | { 50 | "url": "https://getkirby.com/buy", 51 | "type": "custom" 52 | } 53 | ], 54 | "time": "2020-12-28T12:54:39+00:00" 55 | } 56 | ], 57 | "packages-dev": [ 58 | { 59 | "name": "getkirby/cli", 60 | "version": "1.1.1", 61 | "source": { 62 | "type": "git", 63 | "url": "https://github.com/getkirby/cli.git", 64 | "reference": "0e8074b0e6081c9e1a9f31090733b058db15be06" 65 | }, 66 | "dist": { 67 | "type": "zip", 68 | "url": "https://api.github.com/repos/getkirby/cli/zipball/0e8074b0e6081c9e1a9f31090733b058db15be06", 69 | "reference": "0e8074b0e6081c9e1a9f31090733b058db15be06", 70 | "shasum": "" 71 | }, 72 | "require": { 73 | "composer-runtime-api": "^2.2", 74 | "ext-zip": "*", 75 | "guzzlehttp/guzzle": "^7.5", 76 | "league/climate": "^3.8", 77 | "php": ">=8.0.0 <8.3.0" 78 | }, 79 | "bin": [ 80 | "bin/kirby" 81 | ], 82 | "type": "library", 83 | "autoload": { 84 | "psr-4": { 85 | "Kirby\\": [ 86 | "src/", 87 | "tests/" 88 | ] 89 | } 90 | }, 91 | "notification-url": "https://packagist.org/downloads/", 92 | "license": [ 93 | "MIT" 94 | ], 95 | "authors": [ 96 | { 97 | "name": "Kirby Team", 98 | "email": "support@getkirby.com", 99 | "homepage": "https://getkirby.com" 100 | } 101 | ], 102 | "description": "Kirby command line interface", 103 | "homepage": "https://getkirby.com", 104 | "keywords": [ 105 | "cli", 106 | "cms", 107 | "command", 108 | "kirby" 109 | ], 110 | "support": { 111 | "email": "support@getkirby.com", 112 | "forum": "https://forum.getkirby.com", 113 | "issues": "https://github.com/getkirby/cli/issues", 114 | "source": "https://github.com/getkirby/cli" 115 | }, 116 | "funding": [ 117 | { 118 | "url": "https://getkirby.com/buy", 119 | "type": "custom" 120 | } 121 | ], 122 | "time": "2023-02-10T12:27:30+00:00" 123 | }, 124 | { 125 | "name": "guzzlehttp/guzzle", 126 | "version": "7.5.0", 127 | "source": { 128 | "type": "git", 129 | "url": "https://github.com/guzzle/guzzle.git", 130 | "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba" 131 | }, 132 | "dist": { 133 | "type": "zip", 134 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba", 135 | "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba", 136 | "shasum": "" 137 | }, 138 | "require": { 139 | "ext-json": "*", 140 | "guzzlehttp/promises": "^1.5", 141 | "guzzlehttp/psr7": "^1.9 || ^2.4", 142 | "php": "^7.2.5 || ^8.0", 143 | "psr/http-client": "^1.0", 144 | "symfony/deprecation-contracts": "^2.2 || ^3.0" 145 | }, 146 | "provide": { 147 | "psr/http-client-implementation": "1.0" 148 | }, 149 | "require-dev": { 150 | "bamarni/composer-bin-plugin": "^1.8.1", 151 | "ext-curl": "*", 152 | "php-http/client-integration-tests": "^3.0", 153 | "phpunit/phpunit": "^8.5.29 || ^9.5.23", 154 | "psr/log": "^1.1 || ^2.0 || ^3.0" 155 | }, 156 | "suggest": { 157 | "ext-curl": "Required for CURL handler support", 158 | "ext-intl": "Required for Internationalized Domain Name (IDN) support", 159 | "psr/log": "Required for using the Log middleware" 160 | }, 161 | "type": "library", 162 | "extra": { 163 | "bamarni-bin": { 164 | "bin-links": true, 165 | "forward-command": false 166 | }, 167 | "branch-alias": { 168 | "dev-master": "7.5-dev" 169 | } 170 | }, 171 | "autoload": { 172 | "files": [ 173 | "src/functions_include.php" 174 | ], 175 | "psr-4": { 176 | "GuzzleHttp\\": "src/" 177 | } 178 | }, 179 | "notification-url": "https://packagist.org/downloads/", 180 | "license": [ 181 | "MIT" 182 | ], 183 | "authors": [ 184 | { 185 | "name": "Graham Campbell", 186 | "email": "hello@gjcampbell.co.uk", 187 | "homepage": "https://github.com/GrahamCampbell" 188 | }, 189 | { 190 | "name": "Michael Dowling", 191 | "email": "mtdowling@gmail.com", 192 | "homepage": "https://github.com/mtdowling" 193 | }, 194 | { 195 | "name": "Jeremy Lindblom", 196 | "email": "jeremeamia@gmail.com", 197 | "homepage": "https://github.com/jeremeamia" 198 | }, 199 | { 200 | "name": "George Mponos", 201 | "email": "gmponos@gmail.com", 202 | "homepage": "https://github.com/gmponos" 203 | }, 204 | { 205 | "name": "Tobias Nyholm", 206 | "email": "tobias.nyholm@gmail.com", 207 | "homepage": "https://github.com/Nyholm" 208 | }, 209 | { 210 | "name": "Márk Sági-Kazár", 211 | "email": "mark.sagikazar@gmail.com", 212 | "homepage": "https://github.com/sagikazarmark" 213 | }, 214 | { 215 | "name": "Tobias Schultze", 216 | "email": "webmaster@tubo-world.de", 217 | "homepage": "https://github.com/Tobion" 218 | } 219 | ], 220 | "description": "Guzzle is a PHP HTTP client library", 221 | "keywords": [ 222 | "client", 223 | "curl", 224 | "framework", 225 | "http", 226 | "http client", 227 | "psr-18", 228 | "psr-7", 229 | "rest", 230 | "web service" 231 | ], 232 | "support": { 233 | "issues": "https://github.com/guzzle/guzzle/issues", 234 | "source": "https://github.com/guzzle/guzzle/tree/7.5.0" 235 | }, 236 | "funding": [ 237 | { 238 | "url": "https://github.com/GrahamCampbell", 239 | "type": "github" 240 | }, 241 | { 242 | "url": "https://github.com/Nyholm", 243 | "type": "github" 244 | }, 245 | { 246 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", 247 | "type": "tidelift" 248 | } 249 | ], 250 | "time": "2022-08-28T15:39:27+00:00" 251 | }, 252 | { 253 | "name": "guzzlehttp/promises", 254 | "version": "1.5.2", 255 | "source": { 256 | "type": "git", 257 | "url": "https://github.com/guzzle/promises.git", 258 | "reference": "b94b2807d85443f9719887892882d0329d1e2598" 259 | }, 260 | "dist": { 261 | "type": "zip", 262 | "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", 263 | "reference": "b94b2807d85443f9719887892882d0329d1e2598", 264 | "shasum": "" 265 | }, 266 | "require": { 267 | "php": ">=5.5" 268 | }, 269 | "require-dev": { 270 | "symfony/phpunit-bridge": "^4.4 || ^5.1" 271 | }, 272 | "type": "library", 273 | "extra": { 274 | "branch-alias": { 275 | "dev-master": "1.5-dev" 276 | } 277 | }, 278 | "autoload": { 279 | "files": [ 280 | "src/functions_include.php" 281 | ], 282 | "psr-4": { 283 | "GuzzleHttp\\Promise\\": "src/" 284 | } 285 | }, 286 | "notification-url": "https://packagist.org/downloads/", 287 | "license": [ 288 | "MIT" 289 | ], 290 | "authors": [ 291 | { 292 | "name": "Graham Campbell", 293 | "email": "hello@gjcampbell.co.uk", 294 | "homepage": "https://github.com/GrahamCampbell" 295 | }, 296 | { 297 | "name": "Michael Dowling", 298 | "email": "mtdowling@gmail.com", 299 | "homepage": "https://github.com/mtdowling" 300 | }, 301 | { 302 | "name": "Tobias Nyholm", 303 | "email": "tobias.nyholm@gmail.com", 304 | "homepage": "https://github.com/Nyholm" 305 | }, 306 | { 307 | "name": "Tobias Schultze", 308 | "email": "webmaster@tubo-world.de", 309 | "homepage": "https://github.com/Tobion" 310 | } 311 | ], 312 | "description": "Guzzle promises library", 313 | "keywords": [ 314 | "promise" 315 | ], 316 | "support": { 317 | "issues": "https://github.com/guzzle/promises/issues", 318 | "source": "https://github.com/guzzle/promises/tree/1.5.2" 319 | }, 320 | "funding": [ 321 | { 322 | "url": "https://github.com/GrahamCampbell", 323 | "type": "github" 324 | }, 325 | { 326 | "url": "https://github.com/Nyholm", 327 | "type": "github" 328 | }, 329 | { 330 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", 331 | "type": "tidelift" 332 | } 333 | ], 334 | "time": "2022-08-28T14:55:35+00:00" 335 | }, 336 | { 337 | "name": "guzzlehttp/psr7", 338 | "version": "2.4.4", 339 | "source": { 340 | "type": "git", 341 | "url": "https://github.com/guzzle/psr7.git", 342 | "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf" 343 | }, 344 | "dist": { 345 | "type": "zip", 346 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", 347 | "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", 348 | "shasum": "" 349 | }, 350 | "require": { 351 | "php": "^7.2.5 || ^8.0", 352 | "psr/http-factory": "^1.0", 353 | "psr/http-message": "^1.0", 354 | "ralouphie/getallheaders": "^3.0" 355 | }, 356 | "provide": { 357 | "psr/http-factory-implementation": "1.0", 358 | "psr/http-message-implementation": "1.0" 359 | }, 360 | "require-dev": { 361 | "bamarni/composer-bin-plugin": "^1.8.1", 362 | "http-interop/http-factory-tests": "^0.9", 363 | "phpunit/phpunit": "^8.5.29 || ^9.5.23" 364 | }, 365 | "suggest": { 366 | "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" 367 | }, 368 | "type": "library", 369 | "extra": { 370 | "bamarni-bin": { 371 | "bin-links": true, 372 | "forward-command": false 373 | }, 374 | "branch-alias": { 375 | "dev-master": "2.4-dev" 376 | } 377 | }, 378 | "autoload": { 379 | "psr-4": { 380 | "GuzzleHttp\\Psr7\\": "src/" 381 | } 382 | }, 383 | "notification-url": "https://packagist.org/downloads/", 384 | "license": [ 385 | "MIT" 386 | ], 387 | "authors": [ 388 | { 389 | "name": "Graham Campbell", 390 | "email": "hello@gjcampbell.co.uk", 391 | "homepage": "https://github.com/GrahamCampbell" 392 | }, 393 | { 394 | "name": "Michael Dowling", 395 | "email": "mtdowling@gmail.com", 396 | "homepage": "https://github.com/mtdowling" 397 | }, 398 | { 399 | "name": "George Mponos", 400 | "email": "gmponos@gmail.com", 401 | "homepage": "https://github.com/gmponos" 402 | }, 403 | { 404 | "name": "Tobias Nyholm", 405 | "email": "tobias.nyholm@gmail.com", 406 | "homepage": "https://github.com/Nyholm" 407 | }, 408 | { 409 | "name": "Márk Sági-Kazár", 410 | "email": "mark.sagikazar@gmail.com", 411 | "homepage": "https://github.com/sagikazarmark" 412 | }, 413 | { 414 | "name": "Tobias Schultze", 415 | "email": "webmaster@tubo-world.de", 416 | "homepage": "https://github.com/Tobion" 417 | }, 418 | { 419 | "name": "Márk Sági-Kazár", 420 | "email": "mark.sagikazar@gmail.com", 421 | "homepage": "https://sagikazarmark.hu" 422 | } 423 | ], 424 | "description": "PSR-7 message implementation that also provides common utility methods", 425 | "keywords": [ 426 | "http", 427 | "message", 428 | "psr-7", 429 | "request", 430 | "response", 431 | "stream", 432 | "uri", 433 | "url" 434 | ], 435 | "support": { 436 | "issues": "https://github.com/guzzle/psr7/issues", 437 | "source": "https://github.com/guzzle/psr7/tree/2.4.4" 438 | }, 439 | "funding": [ 440 | { 441 | "url": "https://github.com/GrahamCampbell", 442 | "type": "github" 443 | }, 444 | { 445 | "url": "https://github.com/Nyholm", 446 | "type": "github" 447 | }, 448 | { 449 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", 450 | "type": "tidelift" 451 | } 452 | ], 453 | "time": "2023-03-09T13:19:02+00:00" 454 | }, 455 | { 456 | "name": "league/climate", 457 | "version": "3.8.2", 458 | "source": { 459 | "type": "git", 460 | "url": "https://github.com/thephpleague/climate.git", 461 | "reference": "a785a3ac8f584eed4abd45e4e16fe64c46659a28" 462 | }, 463 | "dist": { 464 | "type": "zip", 465 | "url": "https://api.github.com/repos/thephpleague/climate/zipball/a785a3ac8f584eed4abd45e4e16fe64c46659a28", 466 | "reference": "a785a3ac8f584eed4abd45e4e16fe64c46659a28", 467 | "shasum": "" 468 | }, 469 | "require": { 470 | "php": "^7.3 || ^8.0", 471 | "psr/log": "^1.0 || ^2.0 || ^3.0", 472 | "seld/cli-prompt": "^1.0" 473 | }, 474 | "require-dev": { 475 | "mikey179/vfsstream": "^1.6.10", 476 | "mockery/mockery": "^1.4.2", 477 | "phpunit/phpunit": "^9.5.10" 478 | }, 479 | "suggest": { 480 | "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" 481 | }, 482 | "type": "library", 483 | "autoload": { 484 | "psr-4": { 485 | "League\\CLImate\\": "src/" 486 | } 487 | }, 488 | "notification-url": "https://packagist.org/downloads/", 489 | "license": [ 490 | "MIT" 491 | ], 492 | "authors": [ 493 | { 494 | "name": "Joe Tannenbaum", 495 | "email": "hey@joe.codes", 496 | "homepage": "http://joe.codes/", 497 | "role": "Developer" 498 | }, 499 | { 500 | "name": "Craig Duncan", 501 | "email": "git@duncanc.co.uk", 502 | "homepage": "https://github.com/duncan3dc", 503 | "role": "Developer" 504 | } 505 | ], 506 | "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", 507 | "keywords": [ 508 | "cli", 509 | "colors", 510 | "command", 511 | "php", 512 | "terminal" 513 | ], 514 | "support": { 515 | "issues": "https://github.com/thephpleague/climate/issues", 516 | "source": "https://github.com/thephpleague/climate/tree/3.8.2" 517 | }, 518 | "time": "2022-06-18T14:42:08+00:00" 519 | }, 520 | { 521 | "name": "psr/http-client", 522 | "version": "1.0.1", 523 | "source": { 524 | "type": "git", 525 | "url": "https://github.com/php-fig/http-client.git", 526 | "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" 527 | }, 528 | "dist": { 529 | "type": "zip", 530 | "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", 531 | "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", 532 | "shasum": "" 533 | }, 534 | "require": { 535 | "php": "^7.0 || ^8.0", 536 | "psr/http-message": "^1.0" 537 | }, 538 | "type": "library", 539 | "extra": { 540 | "branch-alias": { 541 | "dev-master": "1.0.x-dev" 542 | } 543 | }, 544 | "autoload": { 545 | "psr-4": { 546 | "Psr\\Http\\Client\\": "src/" 547 | } 548 | }, 549 | "notification-url": "https://packagist.org/downloads/", 550 | "license": [ 551 | "MIT" 552 | ], 553 | "authors": [ 554 | { 555 | "name": "PHP-FIG", 556 | "homepage": "http://www.php-fig.org/" 557 | } 558 | ], 559 | "description": "Common interface for HTTP clients", 560 | "homepage": "https://github.com/php-fig/http-client", 561 | "keywords": [ 562 | "http", 563 | "http-client", 564 | "psr", 565 | "psr-18" 566 | ], 567 | "support": { 568 | "source": "https://github.com/php-fig/http-client/tree/master" 569 | }, 570 | "time": "2020-06-29T06:28:15+00:00" 571 | }, 572 | { 573 | "name": "psr/http-factory", 574 | "version": "1.0.1", 575 | "source": { 576 | "type": "git", 577 | "url": "https://github.com/php-fig/http-factory.git", 578 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" 579 | }, 580 | "dist": { 581 | "type": "zip", 582 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 583 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 584 | "shasum": "" 585 | }, 586 | "require": { 587 | "php": ">=7.0.0", 588 | "psr/http-message": "^1.0" 589 | }, 590 | "type": "library", 591 | "extra": { 592 | "branch-alias": { 593 | "dev-master": "1.0.x-dev" 594 | } 595 | }, 596 | "autoload": { 597 | "psr-4": { 598 | "Psr\\Http\\Message\\": "src/" 599 | } 600 | }, 601 | "notification-url": "https://packagist.org/downloads/", 602 | "license": [ 603 | "MIT" 604 | ], 605 | "authors": [ 606 | { 607 | "name": "PHP-FIG", 608 | "homepage": "http://www.php-fig.org/" 609 | } 610 | ], 611 | "description": "Common interfaces for PSR-7 HTTP message factories", 612 | "keywords": [ 613 | "factory", 614 | "http", 615 | "message", 616 | "psr", 617 | "psr-17", 618 | "psr-7", 619 | "request", 620 | "response" 621 | ], 622 | "support": { 623 | "source": "https://github.com/php-fig/http-factory/tree/master" 624 | }, 625 | "time": "2019-04-30T12:38:16+00:00" 626 | }, 627 | { 628 | "name": "psr/http-message", 629 | "version": "1.0.1", 630 | "source": { 631 | "type": "git", 632 | "url": "https://github.com/php-fig/http-message.git", 633 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 634 | }, 635 | "dist": { 636 | "type": "zip", 637 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 638 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 639 | "shasum": "" 640 | }, 641 | "require": { 642 | "php": ">=5.3.0" 643 | }, 644 | "type": "library", 645 | "extra": { 646 | "branch-alias": { 647 | "dev-master": "1.0.x-dev" 648 | } 649 | }, 650 | "autoload": { 651 | "psr-4": { 652 | "Psr\\Http\\Message\\": "src/" 653 | } 654 | }, 655 | "notification-url": "https://packagist.org/downloads/", 656 | "license": [ 657 | "MIT" 658 | ], 659 | "authors": [ 660 | { 661 | "name": "PHP-FIG", 662 | "homepage": "http://www.php-fig.org/" 663 | } 664 | ], 665 | "description": "Common interface for HTTP messages", 666 | "homepage": "https://github.com/php-fig/http-message", 667 | "keywords": [ 668 | "http", 669 | "http-message", 670 | "psr", 671 | "psr-7", 672 | "request", 673 | "response" 674 | ], 675 | "support": { 676 | "source": "https://github.com/php-fig/http-message/tree/master" 677 | }, 678 | "time": "2016-08-06T14:39:51+00:00" 679 | }, 680 | { 681 | "name": "psr/log", 682 | "version": "3.0.0", 683 | "source": { 684 | "type": "git", 685 | "url": "https://github.com/php-fig/log.git", 686 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" 687 | }, 688 | "dist": { 689 | "type": "zip", 690 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", 691 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", 692 | "shasum": "" 693 | }, 694 | "require": { 695 | "php": ">=8.0.0" 696 | }, 697 | "type": "library", 698 | "extra": { 699 | "branch-alias": { 700 | "dev-master": "3.x-dev" 701 | } 702 | }, 703 | "autoload": { 704 | "psr-4": { 705 | "Psr\\Log\\": "src" 706 | } 707 | }, 708 | "notification-url": "https://packagist.org/downloads/", 709 | "license": [ 710 | "MIT" 711 | ], 712 | "authors": [ 713 | { 714 | "name": "PHP-FIG", 715 | "homepage": "https://www.php-fig.org/" 716 | } 717 | ], 718 | "description": "Common interface for logging libraries", 719 | "homepage": "https://github.com/php-fig/log", 720 | "keywords": [ 721 | "log", 722 | "psr", 723 | "psr-3" 724 | ], 725 | "support": { 726 | "source": "https://github.com/php-fig/log/tree/3.0.0" 727 | }, 728 | "time": "2021-07-14T16:46:02+00:00" 729 | }, 730 | { 731 | "name": "ralouphie/getallheaders", 732 | "version": "3.0.3", 733 | "source": { 734 | "type": "git", 735 | "url": "https://github.com/ralouphie/getallheaders.git", 736 | "reference": "120b605dfeb996808c31b6477290a714d356e822" 737 | }, 738 | "dist": { 739 | "type": "zip", 740 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", 741 | "reference": "120b605dfeb996808c31b6477290a714d356e822", 742 | "shasum": "" 743 | }, 744 | "require": { 745 | "php": ">=5.6" 746 | }, 747 | "require-dev": { 748 | "php-coveralls/php-coveralls": "^2.1", 749 | "phpunit/phpunit": "^5 || ^6.5" 750 | }, 751 | "type": "library", 752 | "autoload": { 753 | "files": [ 754 | "src/getallheaders.php" 755 | ] 756 | }, 757 | "notification-url": "https://packagist.org/downloads/", 758 | "license": [ 759 | "MIT" 760 | ], 761 | "authors": [ 762 | { 763 | "name": "Ralph Khattar", 764 | "email": "ralph.khattar@gmail.com" 765 | } 766 | ], 767 | "description": "A polyfill for getallheaders.", 768 | "support": { 769 | "issues": "https://github.com/ralouphie/getallheaders/issues", 770 | "source": "https://github.com/ralouphie/getallheaders/tree/develop" 771 | }, 772 | "time": "2019-03-08T08:55:37+00:00" 773 | }, 774 | { 775 | "name": "seld/cli-prompt", 776 | "version": "1.0.4", 777 | "source": { 778 | "type": "git", 779 | "url": "https://github.com/Seldaek/cli-prompt.git", 780 | "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5" 781 | }, 782 | "dist": { 783 | "type": "zip", 784 | "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5", 785 | "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5", 786 | "shasum": "" 787 | }, 788 | "require": { 789 | "php": ">=5.3" 790 | }, 791 | "require-dev": { 792 | "phpstan/phpstan": "^0.12.63" 793 | }, 794 | "type": "library", 795 | "extra": { 796 | "branch-alias": { 797 | "dev-master": "1.x-dev" 798 | } 799 | }, 800 | "autoload": { 801 | "psr-4": { 802 | "Seld\\CliPrompt\\": "src/" 803 | } 804 | }, 805 | "notification-url": "https://packagist.org/downloads/", 806 | "license": [ 807 | "MIT" 808 | ], 809 | "authors": [ 810 | { 811 | "name": "Jordi Boggiano", 812 | "email": "j.boggiano@seld.be" 813 | } 814 | ], 815 | "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", 816 | "keywords": [ 817 | "cli", 818 | "console", 819 | "hidden", 820 | "input", 821 | "prompt" 822 | ], 823 | "support": { 824 | "issues": "https://github.com/Seldaek/cli-prompt/issues", 825 | "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4" 826 | }, 827 | "time": "2020-12-15T21:32:01+00:00" 828 | }, 829 | { 830 | "name": "symfony/deprecation-contracts", 831 | "version": "v3.2.1", 832 | "source": { 833 | "type": "git", 834 | "url": "https://github.com/symfony/deprecation-contracts.git", 835 | "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" 836 | }, 837 | "dist": { 838 | "type": "zip", 839 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", 840 | "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", 841 | "shasum": "" 842 | }, 843 | "require": { 844 | "php": ">=8.1" 845 | }, 846 | "type": "library", 847 | "extra": { 848 | "branch-alias": { 849 | "dev-main": "3.3-dev" 850 | }, 851 | "thanks": { 852 | "name": "symfony/contracts", 853 | "url": "https://github.com/symfony/contracts" 854 | } 855 | }, 856 | "autoload": { 857 | "files": [ 858 | "function.php" 859 | ] 860 | }, 861 | "notification-url": "https://packagist.org/downloads/", 862 | "license": [ 863 | "MIT" 864 | ], 865 | "authors": [ 866 | { 867 | "name": "Nicolas Grekas", 868 | "email": "p@tchwork.com" 869 | }, 870 | { 871 | "name": "Symfony Community", 872 | "homepage": "https://symfony.com/contributors" 873 | } 874 | ], 875 | "description": "A generic function and convention to trigger deprecation notices", 876 | "homepage": "https://symfony.com", 877 | "support": { 878 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" 879 | }, 880 | "funding": [ 881 | { 882 | "url": "https://symfony.com/sponsor", 883 | "type": "custom" 884 | }, 885 | { 886 | "url": "https://github.com/fabpot", 887 | "type": "github" 888 | }, 889 | { 890 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 891 | "type": "tidelift" 892 | } 893 | ], 894 | "time": "2023-03-01T10:25:55+00:00" 895 | }, 896 | { 897 | "name": "symfony/polyfill-mbstring", 898 | "version": "v1.27.0", 899 | "source": { 900 | "type": "git", 901 | "url": "https://github.com/symfony/polyfill-mbstring.git", 902 | "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" 903 | }, 904 | "dist": { 905 | "type": "zip", 906 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", 907 | "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", 908 | "shasum": "" 909 | }, 910 | "require": { 911 | "php": ">=7.1" 912 | }, 913 | "provide": { 914 | "ext-mbstring": "*" 915 | }, 916 | "suggest": { 917 | "ext-mbstring": "For best performance" 918 | }, 919 | "type": "library", 920 | "extra": { 921 | "branch-alias": { 922 | "dev-main": "1.27-dev" 923 | }, 924 | "thanks": { 925 | "name": "symfony/polyfill", 926 | "url": "https://github.com/symfony/polyfill" 927 | } 928 | }, 929 | "autoload": { 930 | "files": [ 931 | "bootstrap.php" 932 | ], 933 | "psr-4": { 934 | "Symfony\\Polyfill\\Mbstring\\": "" 935 | } 936 | }, 937 | "notification-url": "https://packagist.org/downloads/", 938 | "license": [ 939 | "MIT" 940 | ], 941 | "authors": [ 942 | { 943 | "name": "Nicolas Grekas", 944 | "email": "p@tchwork.com" 945 | }, 946 | { 947 | "name": "Symfony Community", 948 | "homepage": "https://symfony.com/contributors" 949 | } 950 | ], 951 | "description": "Symfony polyfill for the Mbstring extension", 952 | "homepage": "https://symfony.com", 953 | "keywords": [ 954 | "compatibility", 955 | "mbstring", 956 | "polyfill", 957 | "portable", 958 | "shim" 959 | ], 960 | "support": { 961 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" 962 | }, 963 | "funding": [ 964 | { 965 | "url": "https://symfony.com/sponsor", 966 | "type": "custom" 967 | }, 968 | { 969 | "url": "https://github.com/fabpot", 970 | "type": "github" 971 | }, 972 | { 973 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 974 | "type": "tidelift" 975 | } 976 | ], 977 | "time": "2022-11-03T14:55:06+00:00" 978 | }, 979 | { 980 | "name": "symfony/var-dumper", 981 | "version": "v6.2.7", 982 | "source": { 983 | "type": "git", 984 | "url": "https://github.com/symfony/var-dumper.git", 985 | "reference": "cf8d4ca1ddc1e3cc242375deb8fc23e54f5e2a1e" 986 | }, 987 | "dist": { 988 | "type": "zip", 989 | "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cf8d4ca1ddc1e3cc242375deb8fc23e54f5e2a1e", 990 | "reference": "cf8d4ca1ddc1e3cc242375deb8fc23e54f5e2a1e", 991 | "shasum": "" 992 | }, 993 | "require": { 994 | "php": ">=8.1", 995 | "symfony/polyfill-mbstring": "~1.0" 996 | }, 997 | "conflict": { 998 | "phpunit/phpunit": "<5.4.3", 999 | "symfony/console": "<5.4" 1000 | }, 1001 | "require-dev": { 1002 | "ext-iconv": "*", 1003 | "symfony/console": "^5.4|^6.0", 1004 | "symfony/process": "^5.4|^6.0", 1005 | "symfony/uid": "^5.4|^6.0", 1006 | "twig/twig": "^2.13|^3.0.4" 1007 | }, 1008 | "suggest": { 1009 | "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", 1010 | "ext-intl": "To show region name in time zone dump", 1011 | "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" 1012 | }, 1013 | "bin": [ 1014 | "Resources/bin/var-dump-server" 1015 | ], 1016 | "type": "library", 1017 | "autoload": { 1018 | "files": [ 1019 | "Resources/functions/dump.php" 1020 | ], 1021 | "psr-4": { 1022 | "Symfony\\Component\\VarDumper\\": "" 1023 | }, 1024 | "exclude-from-classmap": [ 1025 | "/Tests/" 1026 | ] 1027 | }, 1028 | "notification-url": "https://packagist.org/downloads/", 1029 | "license": [ 1030 | "MIT" 1031 | ], 1032 | "authors": [ 1033 | { 1034 | "name": "Nicolas Grekas", 1035 | "email": "p@tchwork.com" 1036 | }, 1037 | { 1038 | "name": "Symfony Community", 1039 | "homepage": "https://symfony.com/contributors" 1040 | } 1041 | ], 1042 | "description": "Provides mechanisms for walking through any arbitrary PHP variable", 1043 | "homepage": "https://symfony.com", 1044 | "keywords": [ 1045 | "debug", 1046 | "dump" 1047 | ], 1048 | "support": { 1049 | "source": "https://github.com/symfony/var-dumper/tree/v6.2.7" 1050 | }, 1051 | "funding": [ 1052 | { 1053 | "url": "https://symfony.com/sponsor", 1054 | "type": "custom" 1055 | }, 1056 | { 1057 | "url": "https://github.com/fabpot", 1058 | "type": "github" 1059 | }, 1060 | { 1061 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1062 | "type": "tidelift" 1063 | } 1064 | ], 1065 | "time": "2023-02-24T10:42:00+00:00" 1066 | } 1067 | ], 1068 | "aliases": [], 1069 | "minimum-stability": "stable", 1070 | "stability-flags": [], 1071 | "prefer-stable": false, 1072 | "prefer-lowest": false, 1073 | "platform": { 1074 | "php": ">=8.1" 1075 | }, 1076 | "platform-dev": [], 1077 | "plugin-api-version": "2.3.0" 1078 | } 1079 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'host' => 'localhost:8108', 15 | 'num_typos' => 2, 16 | ], 17 | 'siteMethods' => [ 18 | 'typesenseSearch' => function (string $query, int $limit = 30, int $page = 1): SearchResult { 19 | $searchEngine = new TypesenseSearch(); 20 | return $searchEngine->search($query, $limit, $page); 21 | } 22 | ], 23 | 'hooks' => [ 24 | 'page.update:after' => function (Page $newPage, Page $oldPage) { 25 | 26 | if (TypesenseConfig::isIndexable($newPage)) { 27 | $document = new TypesenseDocument($newPage); 28 | $document 29 | ->setNormalizer(TypesenseConfig::getNormalizer($newPage)) 30 | ->upsert(); 31 | } 32 | }, 33 | 'page.changeTitle:after' => function (Page $newPage) { 34 | if (TypesenseConfig::isIndexable($newPage)) { 35 | $document = new TypesenseDocument($newPage); 36 | $document 37 | ->setNormalizer(TypesenseConfig::getNormalizer($newPage)) 38 | ->upsert(); 39 | } 40 | }, 41 | 'page.delete:after' => function (bool $status, Page $page) { 42 | if ($status) { 43 | $document = new TypesenseDocument($page); 44 | $document->delete(); 45 | } 46 | }, 47 | 'page.changeTemplate:after' => function (Page $newPage) { 48 | 49 | $document = new TypesenseDocument($newPage); 50 | // if new template is not indexable, remove document 51 | if (!TypesenseConfig::isIndexable($newPage)) { 52 | $document->delete(); 53 | } else { 54 | $document 55 | ->setNormalizer(TypesenseConfig::getNormalizer($newPage)) 56 | ->upsert(); 57 | } 58 | 59 | }, 60 | 'page.changeStatus:after' => function (Page $newPage, Page $oldPage) { 61 | 62 | // if new status is published and page is indexable, upsert 63 | if (TypesenseConfig::isIndexable($newPage)) { 64 | $document = new TypesenseDocument($newPage); 65 | $document 66 | ->setNormalizer(TypesenseConfig::getNormalizer($newPage)) 67 | ->upsert(); 68 | } 69 | 70 | // if new status is unpublished, delete document from typesense index 71 | if ($newPage->isDraft()) { 72 | $document = new TypesenseDocument($newPage); 73 | $document->delete(); 74 | } 75 | 76 | } 77 | ], 78 | 'commands' => [ 79 | 'typesense:rebuild' => [ 80 | 'description' => 'Rebuild typesense index, even if you change fields configuration', 81 | 'args' => [], 82 | 'command' => function (Kirby\CLI\CLI $cli) { 83 | // TODO build command 84 | $cli->success('hello world'); 85 | 86 | } 87 | ] 88 | ] 89 | ]); 90 | -------------------------------------------------------------------------------- /src/SearchResult.php: -------------------------------------------------------------------------------- 1 | items; 21 | } 22 | 23 | public function getTotal(): int 24 | { 25 | return $this->resultsCount; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/TypesenseClient.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 23 | $this->host = $host; 24 | } 25 | 26 | public function get(string $endpoint): array 27 | { 28 | return $this->request($endpoint); 29 | } 30 | 31 | public function post(string $endpoint, array $data = []): array 32 | { 33 | return $this->request($endpoint, $data, 'POST'); 34 | } 35 | 36 | public function patch(string $endpoint, array $data = []): array 37 | { 38 | return $this->request($endpoint, $data, 'PATCH'); 39 | } 40 | 41 | public function delete(string $endpoint, array $data = []): array 42 | { 43 | return $this->request($endpoint, $data, 'DELETE'); 44 | } 45 | 46 | public function request(string $endpoint, array $data = [], string $method = 'GET'): array 47 | { 48 | 49 | $requestParams = [ 50 | 'headers' => [ 51 | 'X-TYPESENSE-API-KEY' => $this->apiKey, 52 | 'Content-Type' => 'application/json' 53 | ], 54 | 'method' => $method, 55 | 'data' => json_encode($data) 56 | ]; 57 | $response = Remote::request("{$this->host}/collections/{$endpoint}", $requestParams); 58 | 59 | if (($response->code() >= 200 && $response->code() < 300) || $method === 'DELETE') { 60 | return $response->json(); 61 | } 62 | 63 | throw new TypesenseException($response); 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/TypesenseCommand.php: -------------------------------------------------------------------------------- 1 | template()->name(), self::getTemplates()) && $page->isPublished(); 31 | } 32 | 33 | public static function getNormalizer(Page $page): Closure 34 | { 35 | return self::getConfig()[$page->template()->name()]; 36 | } 37 | 38 | public static function getFields(): array 39 | { 40 | $fields = ['title']; 41 | $config = option('maxchene.typesense.schema.fields'); 42 | if (is_array($config)) { 43 | $configFields = A::pluck($config, 'name'); 44 | $fields = array_merge($fields, $configFields); 45 | } 46 | 47 | return $fields; 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/TypesenseDocument.php: -------------------------------------------------------------------------------- 1 | client = new TypesenseClient(); 22 | $this->buildSchema(); 23 | } 24 | 25 | /** 26 | * Upsert is a shortcut for either update or insert 27 | * Meaning that if a document with current id already exists, document will be updated 28 | * and if not, a new document will be created 29 | * 30 | * Note that id and title fields are mandatory 31 | * and always added to fields you provided in config file 32 | * 33 | * @return void 34 | * @throws \Kirby\Exception\InvalidArgumentException 35 | */ 36 | public function upsert(): void 37 | { 38 | 39 | if (empty($this->normalizer)) { 40 | throw new RuntimeException('Missing normalizer closure for Page template ' . $this->page->template()->name()); 41 | } 42 | 43 | $data = ($this->normalizer)($this->page); 44 | $data = array_merge($data, [ 45 | 'id' => $this->page->uuid()->id(), 46 | 'title' => $this->page->title()->value(), 47 | ]); 48 | 49 | $endpoint = $this->getCollectionName() . '/documents?action=upsert'; 50 | try { 51 | $this->client->post($endpoint, $data); 52 | } catch (TypesenseException $exception) { 53 | if ($exception->status === 404 && 'Not Found' === $exception->message) { 54 | $this->createCollection(); 55 | $this->client->post($endpoint, $data); 56 | } else { 57 | throw $exception; 58 | } 59 | } 60 | } 61 | 62 | public function setNormalizer(Closure $normalizer): TypesenseDocument 63 | { 64 | $this->normalizer = $normalizer; 65 | return $this; 66 | } 67 | 68 | private function createCollection(): void 69 | { 70 | $this->client->post('', $this->schema); 71 | } 72 | 73 | private function buildSchema(): void 74 | { 75 | $schema = option('maxchene.typesense.schema'); 76 | if (empty($schema)) { 77 | throw new RuntimeException('Schema configuration is missing from config file'); 78 | } 79 | 80 | if (empty($schema['name'])) { 81 | throw new RuntimeException('Collection name is missing from config file'); 82 | } 83 | 84 | $schema['fields'][] = ['name' => 'title', 'type' => 'string']; 85 | $this->schema = $schema; 86 | } 87 | 88 | private function getCollectionName(): string 89 | { 90 | return $this->schema['name']; 91 | } 92 | 93 | /** 94 | * Delete document from Typesense index 95 | * so that it won't show up in search results 96 | * 97 | * @return void 98 | */ 99 | public function delete(): void 100 | { 101 | $endpoint = $this->getCollectionName() . '/documents/' . $this->page->uuid()->id(); 102 | $this->client->delete($endpoint); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/TypesenseException.php: -------------------------------------------------------------------------------- 1 | status = $response->code(); 22 | $this->message = json_decode($response->content(), true, 512, JSON_THROW_ON_ERROR)['message'] ?? ''; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TypesenseItem.php: -------------------------------------------------------------------------------- 1 | html element surrounding keywords", 27 | * } 28 | * ] 29 | */ 30 | } 31 | 32 | /** 33 | * Get Kirby page UUID, without page:// prefix 34 | * @return string 35 | */ 36 | public function getId(): string 37 | { 38 | return $this->item['document']['id']; 39 | } 40 | 41 | /** 42 | * Get search result title with tag 43 | * @return string 44 | */ 45 | public function getTitle(): string 46 | { 47 | return $this->get('title'); 48 | } 49 | 50 | /** 51 | * Get Kirby Page from uuid 52 | * @return Page|null 53 | */ 54 | public function getPage(): Page|null 55 | { 56 | return App::instance()->site()->index()->findBy('uuid', 'page://' . $this->getId()); 57 | } 58 | 59 | public function get(string $field): string 60 | { 61 | foreach ($this->item['highlights'] as $highlight) { 62 | if ($field === $highlight['field']) { 63 | return $highlight['snippet']; 64 | } 65 | } 66 | return ''; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/TypesenseItemInterface.php: -------------------------------------------------------------------------------- 1 | client = new TypesenseClient(); 19 | $collection = TypesenseConfig::getCollectionName(); 20 | if (empty($collection)) { 21 | throw new RuntimeException('Missing collection name from config file.'); 22 | } 23 | $this->searchUrl = "{$collection}/documents/search"; 24 | } 25 | 26 | public function search(string $q, int $limit = 30, int $page = 1) 27 | { 28 | 29 | $fields = A::join(TypesenseConfig::getFields(), ','); 30 | $query = new Query([ 31 | 'q' => $q, 32 | 'query_by' => $fields, 33 | 'num_typos' => option('maxchene.typesense.num_typos'), 34 | 'per_page' => $limit, 35 | 'page' => $page 36 | ]); 37 | $url = "{$this->searchUrl}{$query->toString(true)}"; 38 | ['found' => $resultsCount, 'hits' => $searchItems] = $this->client->get($url); 39 | return new SearchResult(array_map(fn(array $item) => new TypesenseItem($item), $searchItems), $resultsCount); 40 | } 41 | 42 | } 43 | --------------------------------------------------------------------------------