├── .gitignore ├── README.md ├── docs ├── 001-introduction.md ├── 002-install-redisearch.md ├── 003-create-index.md ├── 004-query-data.md ├── 005-manage-index.md ├── 006-import-dataset.md ├── 007-query-movies.md ├── 008-aggregation.md ├── 009-advanced-features.md ├── 010-application-development.md └── images │ ├── logo.svg │ ├── sample-app-archi.png │ └── secondary-index.png └── sample-app ├── README.md ├── docker-compose.yaml ├── front-end ├── .browserslistrc ├── .env.development ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── imgs │ │ ├── forkme_left_red.svg │ │ ├── redis-logo.svg │ │ ├── redis.svg │ │ └── redislabs.png │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── Comments.vue │ ├── lib │ │ ├── SearchClient.js │ │ └── search-samples.json │ ├── main.js │ ├── router │ │ └── index.js │ └── views │ │ ├── FacetedSearch.vue │ │ ├── Home.vue │ │ ├── MovieForm.vue │ │ └── Search.vue └── vue.config.js ├── redisearch-docker └── dataset │ ├── import-data.sh │ ├── import_actors.redis │ ├── import_create_index.redis │ ├── import_movies.redis │ ├── import_theaters.redis │ └── import_users.redis ├── redisearch-jedis-rest ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── redislabs │ │ └── search │ │ └── demo │ │ └── jedis │ │ ├── RediSearchRestController.java │ │ ├── RediSearchService.java │ │ ├── RedisearchJedisRestApplication.java │ │ └── util │ │ └── RediSearchCommands.java │ └── resources │ └── application.properties ├── redisearch-node-rest ├── Dockerfile ├── NodeSearchService.js ├── README.md ├── package-lock.json ├── package.json └── server.js └── redisearch-python-rest ├── .flaskenv ├── Dockerfile ├── README.md ├── requirements.txt └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | .idea 5 | *.iml 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Generated files 15 | .idea/**/contentModel.xml 16 | 17 | # Sensitive or high-churn files 18 | .idea/**/dataSources/ 19 | .idea/**/dataSources.ids 20 | .idea/**/dataSources.local.xml 21 | .idea/**/sqlDataSources.xml 22 | .idea/**/dynamic.xml 23 | .idea/**/uiDesigner.xml 24 | .idea/**/dbnavigator.xml 25 | 26 | # Gradle 27 | .idea/**/gradle.xml 28 | .idea/**/libraries 29 | 30 | # Gradle and Maven with auto-import 31 | # When using Gradle or Maven with auto-import, you should exclude module files, 32 | # since they will be recreated, and may cause churn. Uncomment if using 33 | # auto-import. 34 | .idea/artifacts 35 | .idea/compiler.xml 36 | .idea/jarRepositories.xml 37 | .idea/modules.xml 38 | .idea/*.iml 39 | .idea/modules 40 | *.iml 41 | *.ipr 42 | 43 | # CMake 44 | cmake-build-*/ 45 | 46 | # Mongo Explorer plugin 47 | .idea/**/mongoSettings.xml 48 | 49 | # File-based project format 50 | *.iws 51 | 52 | # IntelliJ 53 | out/ 54 | 55 | # mpeltonen/sbt-idea plugin 56 | .idea_modules/ 57 | 58 | # JIRA plugin 59 | atlassian-ide-plugin.xml 60 | 61 | # Cursive Clojure plugin 62 | .idea/replstate.xml 63 | 64 | # Crashlytics plugin (for Android Studio and IntelliJ) 65 | com_crashlytics_export_strings.xml 66 | crashlytics.properties 67 | crashlytics-build.properties 68 | fabric.properties 69 | 70 | # Editor-based Rest Client 71 | .idea/httpRequests 72 | 73 | # Android studio 3.1+ serialized cache file 74 | .idea/caches/build_file_checksums.ser 75 | 76 | 77 | target/ 78 | pom.xml.tag 79 | pom.xml.releaseBackup 80 | pom.xml.versionsBackup 81 | pom.xml.next 82 | release.properties 83 | dependency-reduced-pom.xml 84 | buildNumber.properties 85 | .mvn/timing.properties 86 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 87 | .mvn/wrapper/maven-wrapper.jar 88 | 89 | # General 90 | .DS_Store 91 | .AppleDouble 92 | .LSOverride 93 | 94 | # Icon must end with two \r 95 | Icon 96 | 97 | 98 | # Thumbnails 99 | ._* 100 | 101 | # Files that might appear in the root of a volume 102 | .DocumentRevisions-V100 103 | .fseventsd 104 | .Spotlight-V100 105 | .TemporaryItems 106 | .Trashes 107 | .VolumeIcon.icns 108 | .com.apple.timemachine.donotpresent 109 | 110 | # Directories potentially created on remote AFP share 111 | .AppleDB 112 | .AppleDesktop 113 | Network Trash Folder 114 | Temporary Items 115 | .apdisk 116 | 117 | # Logs 118 | logs 119 | *.log 120 | npm-debug.log* 121 | yarn-debug.log* 122 | yarn-error.log* 123 | lerna-debug.log* 124 | 125 | # Diagnostic reports (https://nodejs.org/api/report.html) 126 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 127 | 128 | # Runtime data 129 | pids 130 | *.pid 131 | *.seed 132 | *.pid.lock 133 | 134 | # Directory for instrumented libs generated by jscoverage/JSCover 135 | lib-cov 136 | 137 | # Coverage directory used by tools like istanbul 138 | coverage 139 | *.lcov 140 | 141 | # nyc test coverage 142 | .nyc_output 143 | 144 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 145 | .grunt 146 | 147 | # Bower dependency directory (https://bower.io/) 148 | bower_components 149 | 150 | # node-waf configuration 151 | .lock-wscript 152 | 153 | # Compiled binary addons (https://nodejs.org/api/addons.html) 154 | build/Release 155 | 156 | # Dependency directories 157 | node_modules/ 158 | jspm_packages/ 159 | 160 | # Snowpack dependency directory (https://snowpack.dev/) 161 | web_modules/ 162 | 163 | # TypeScript cache 164 | *.tsbuildinfo 165 | 166 | # Optional npm cache directory 167 | .npm 168 | 169 | # Optional eslint cache 170 | .eslintcache 171 | 172 | # Microbundle cache 173 | .rpt2_cache/ 174 | .rts2_cache_cjs/ 175 | .rts2_cache_es/ 176 | .rts2_cache_umd/ 177 | 178 | # Optional REPL history 179 | .node_repl_history 180 | 181 | # Output of 'npm pack' 182 | *.tgz 183 | 184 | # Yarn Integrity file 185 | .yarn-integrity 186 | 187 | # dotenv environment variables file 188 | .env 189 | .env.test 190 | 191 | # parcel-bundler cache (https://parceljs.org/) 192 | .cache 193 | .parcel-cache 194 | 195 | # Next.js build output 196 | .next 197 | out 198 | 199 | # Nuxt.js build / generate output 200 | .nuxt 201 | dist 202 | 203 | # Gatsby files 204 | .cache/ 205 | # Comment in the public line in if your project uses Gatsby and not Next.js 206 | # https://nextjs.org/blog/next-9-1#public-directory-support 207 | # public 208 | 209 | # vuepress build output 210 | .vuepress/dist 211 | 212 | # Serverless directories 213 | .serverless/ 214 | 215 | # FuseBox cache 216 | .fusebox/ 217 | 218 | # DynamoDB Local files 219 | .dynamodb/ 220 | 221 | # TernJS port file 222 | .tern-port 223 | 224 | # Stores VSCode versions used for testing VSCode extensions 225 | .vscode-test 226 | 227 | # yarn v2 228 | .yarn/cache 229 | .yarn/unplugged 230 | .yarn/build-state.yml 231 | .yarn/install-state.gz 232 | .pnp.* 233 | 234 | 235 | 236 | # PYTHON 237 | # Byte-compiled / optimized / DLL files 238 | __pycache__/ 239 | *.py[cod] 240 | *$py.class 241 | 242 | # C extensions 243 | *.so 244 | 245 | # Distribution / packaging 246 | .Python 247 | build/ 248 | develop-eggs/ 249 | dist/ 250 | downloads/ 251 | eggs/ 252 | .eggs/ 253 | lib/ 254 | lib64/ 255 | parts/ 256 | sdist/ 257 | var/ 258 | wheels/ 259 | share/python-wheels/ 260 | *.egg-info/ 261 | .installed.cfg 262 | *.egg 263 | MANIFEST 264 | 265 | # PyInstaller 266 | # Usually these files are written by a python script from a template 267 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 268 | *.manifest 269 | *.spec 270 | 271 | # Installer logs 272 | pip-log.txt 273 | pip-delete-this-directory.txt 274 | 275 | # Unit test / coverage reports 276 | htmlcov/ 277 | .tox/ 278 | .nox/ 279 | .coverage 280 | .coverage.* 281 | .cache 282 | nosetests.xml 283 | coverage.xml 284 | *.cover 285 | *.py,cover 286 | .hypothesis/ 287 | .pytest_cache/ 288 | cover/ 289 | 290 | # Translations 291 | *.mo 292 | *.pot 293 | 294 | # Django stuff: 295 | *.log 296 | local_settings.py 297 | db.sqlite3 298 | db.sqlite3-journal 299 | 300 | # Flask stuff: 301 | instance/ 302 | .webassets-cache 303 | 304 | # Scrapy stuff: 305 | .scrapy 306 | 307 | # Sphinx documentation 308 | docs/_build/ 309 | 310 | # PyBuilder 311 | .pybuilder/ 312 | target/ 313 | 314 | # Jupyter Notebook 315 | .ipynb_checkpoints 316 | 317 | # IPython 318 | profile_default/ 319 | ipython_config.py 320 | 321 | # pyenv 322 | # For a library or package, you might want to ignore these files since the code is 323 | # intended to run in multiple environments; otherwise, check them in: 324 | # .python-version 325 | 326 | # pipenv 327 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 328 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 329 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 330 | # install all needed dependencies. 331 | #Pipfile.lock 332 | 333 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 334 | __pypackages__/ 335 | 336 | # Celery stuff 337 | celerybeat-schedule 338 | celerybeat.pid 339 | 340 | # SageMath parsed files 341 | *.sage.py 342 | 343 | # Environments 344 | .env 345 | .venv 346 | env/ 347 | venv/ 348 | ENV/ 349 | env.bak/ 350 | venv.bak/ 351 | 352 | # Spyder project settings 353 | .spyderproject 354 | .spyproject 355 | 356 | # Rope project settings 357 | .ropeproject 358 | 359 | # mkdocs documentation 360 | /site 361 | 362 | # mypy 363 | .mypy_cache/ 364 | .dmypy.json 365 | dmypy.json 366 | 367 | # Pyre type checker 368 | .pyre/ 369 | 370 | # pytype static type analyzer 371 | .pytype/ 372 | 373 | # Cython debug symbols 374 | cython_debug/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Forum](https://img.shields.io/badge/Forum-RediSearch-blue)](https://forum.redislabs.com/c/modules/redisearch/) 2 | [![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](https://discord.gg/xTbqgTB) 3 | 4 | # Getting Started with RediSearch 2.0 5 | 6 | 7 | 8 | 9 | [RediSearch](https://redisearch.io) is a real-time indexing and search engine. 10 | 11 | This project will let you discover the new major release (2.0) and see how to use it, in your application. 12 | 13 | 1. [Introduction](docs/001-introduction.md) (2mn) 14 | 1. [Install RediSearch 2.0 & Insert Data](docs/002-install-redisearch.md) (2mn) 15 | 1. [Create Index](docs/003-create-index.md) (5mn) 16 | 1. [Query Data](docs/004-query-data.md) (15mn) 17 | 1. [Manage Indexes](docs/005-manage-index.md) (5mn) 18 | 1. [Import Sample Dataset](docs/006-import-dataset.md) (2mn) 19 | 1. [Querying the Movie Dataset](docs/007-query-movies.md) (20mn) 20 | 1. [Aggregation](docs/008-aggregation.md) (15mn) 21 | 1. [Advanced Options](docs/009-advanced-features.md) (5mn) 22 | 1. [Sample Application](docs/010-application-development.md) 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/001-introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | In this tutorial you will learn how to use the [redis-stack](https://redis.io/docs/stack/get-started/install/docker/) image to use the [redisearch module](https://redis.io/docs/stack/search/) that provides an indexing and full text search engine for Redis. 4 | 5 | RediSearch provides a simple and fast way to index and query data using any field (secondary index), and do search and aggregation on an indexed dataset. 6 | 7 | --- 8 | Next: [Install RediSearch 2.0 & Insert Data](002-install-redisearch.md) 9 | -------------------------------------------------------------------------------- /docs/002-install-redisearch.md: -------------------------------------------------------------------------------- 1 | # Install RediSearch 2 | 3 | You have multiple ways to run RediSearch: 4 | 5 | * building from [sources](https://github.com/RediSearch/RediSearch) and installing it inside an existing Redis Instance 6 | * using [Redis Cloud](https://redislabs.com/redis-enterprise-cloud/) _(when RediSearch module 2.0 available)_ 7 | * using [Redis Enterprise](https://redislabs.com/redis-enterprise-software/) _(when RediSearch module 2.0 available)_ 8 | * using [Docker](https://hub.docker.com/r/redis/redis-stack) 9 | 10 | Let's use Docker for now. 11 | 12 | **1.1 Open a terminal an run the following command** 13 | 14 | 15 | ``` 16 | > docker run -it --rm --name redis-stack \ 17 | -p 6379:6379 \ 18 | redis/redis-stack:latest 19 | ``` 20 | 21 | *Note: The container will automatically be removed when it exits (`--rm` parameter).* 22 | 23 | You have now a Redis instance running with RediSearch installed, let's discover the basics. 24 | 25 | 26 | --- 27 | Next: [Create Index](003-create-index.md) 28 | -------------------------------------------------------------------------------- /docs/003-create-index.md: -------------------------------------------------------------------------------- 1 | # Create Index 2 | 3 | Before creating the index let's describe the dataset and insert entries. 4 | 5 | ## Sample Dataset 6 | 7 | In this project you will use a simple dataset describing movies, for now, all records are in English. You will learn more about other languages in another tutorial. 8 | 9 | A movie is represented by the following attributes: 10 | 11 | * **`movie_id`** : The unique ID of the movie, internal to this database 12 | * **`title`** : The title of the movie. 13 | * **`plot`** : A summary of the movie. 14 | * **`genre`** : The genre of the movie, for now a movie will only have a single genre. 15 | * **`release_year`** : The year the movie was released as a numerical value. 16 | * **`rating`** : A numeric value representing the public's rating for this movie. 17 | * **`votes`** : Number of votes. 18 | * **`poster`** : Link to the movie poster. 19 | * **`imdb_id`** : id of the movie in the [IMDB](https://imdb.com) database. 20 | 21 | 22 | ### Key and Data structure 23 | 24 | As a Redis developer, one of the first things to look when building your application is to define the structure of the key and data (data design/data modeling). 25 | 26 | A common way of defining the keys in Redis is to use specific patterns in them. For example in this application where the database will probably deal with various business objects: movies, actors, theaters, users, ... we can use the following pattern: 27 | 28 | * `business_object:key` 29 | 30 | For example: 31 | * `movie:001` for the movie with the id 001 32 | * `user:001` the user with the id 001 33 | 34 | 35 | and for the movies information you should use a Redis [Hash](https://redis.io/topics/data-types#hashes). 36 | 37 | A Redis Hash allows the application to structure all the movie attributes in individual fields; also RediSearch will index the fields based on the index definition. 38 | 39 | ## Insert Movies 40 | 41 | 42 | It is time now to add some data into your database, let's insert a few movies, using `redis-cli` or [Redis Insight](https://redislabs.com/redisinsight/). 43 | 44 | Once you are connected to your Redis instance run the following commands: 45 | 46 | ``` 47 | 48 | > HSET movie:11002 title "Star Wars: Episode V - The Empire Strikes Back" plot "After the Rebels are brutally overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jedi training with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy." release_year 1980 genre "Action" rating 8.7 votes 1127635 imdb_id tt0080684 49 | 50 | > HSET movie:11003 title "The Godfather" plot "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son." release_year 1972 genre "Drama" rating 9.2 votes 1563839 imdb_id tt0068646 51 | 52 | > HSET movie:11004 title "Heat" plot "A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist." release_year 1995 genre "Thriller" rating 8.2 votes 559490 imdb_id tt0113277 53 | 54 | > HSET "movie:11005" title "Star Wars: Episode VI - Return of the Jedi" genre "Action" votes 906260 rating 8.3 release_year 1983 plot "The Rebels dispatch to Endor to destroy the second Empire's Death Star." ibmdb_id "tt0086190" 55 | 56 | ``` 57 | 58 | Now it is possible to get information from the hash using the movie ID. For example if you want to get the title, and rating execute the following command: 59 | 60 | ``` 61 | > HMGET movie:11002 title rating 62 | 63 | 1) "Star Wars: Episode V - The Empire Strikes Back" 64 | 2) "8.7" 65 | ``` 66 | 67 | And you can increment the rating of this movie using: 68 | 69 | ``` 70 | > HINCRBYFLOAT movie:11002 rating 0.1 71 | "8.8" 72 | ``` 73 | 74 | But how do you get a movie or list of movies by year of release, rating or title? 75 | 76 | One option, would be to read all the movies, check all fields and then return only matching movies; no need to say that this is a really bad idea. 77 | 78 | Nevertheless this is where Redis developers often create custom secondary indexes using SET/SORTED SET structures that point back to the movie hash. This needs some heavy design and implementation. 79 | 80 | This is where the RediSearch module can help, and why it was created. 81 | 82 | 83 | ## RediSearch & Indexing 84 | 85 | 86 | RediSearch greatly simplifies this by offering a simple and automatic way to create secondary indices on Redis Hashes. (more datastructure will eventually come) 87 | 88 | ![Secondary Index](https://github.com/RediSearch/redisearch-getting-started/blob/master/docs/images/secondary-index.png?raw=true) 89 | 90 | Using RediSearch if you want to query on a field, you must first index that field. Let's start by indexing the following fields for our movies: 91 | 92 | * Title 93 | * Release Year 94 | * Rating 95 | * Genre 96 | 97 | When creating a index you define: 98 | 99 | * which data you want to index: all *hashes* with a key starting with `movies` 100 | * which fields in the hashes you want to index using a Schema definition. 101 | 102 | > ***Warning: Do not index all fields*** 103 | > 104 | > Indexes take space in memory, and must be updated when the primary data is updated. So create the index carefully and keep the definition up to date with your needs. 105 | 106 | ### Create the Index 107 | 108 | Create the index with the following command: 109 | 110 | ``` 111 | > FT.CREATE idx:movie ON hash PREFIX 1 "movie:" SCHEMA title TEXT SORTABLE release_year NUMERIC SORTABLE rating NUMERIC SORTABLE genre TAG SORTABLE 112 | ``` 113 | 114 | Before running some queries let's look at the command in detail: 115 | 116 | * [`FT.CREATE`](https://oss.redislabs.com/redisearch/master/Commands/#ftcreate) : creates an index with the given spec. The index name will be used in all the key names so keep it short. 117 | * `idx:movie` : the name of the index 118 | * `ON hash` : the type of structure to be indexed. *Note that in RediSearch 2.0 only hash structures are supported, this parameter will accept other Redis data types in future as RediSearch is updated to index them* 119 | * `PREFIX 1 "movie:"` : the prefix of the keys that should be indexed. This is a list, so since we want to only index movie:* keys the number is 1. Suppose you want to index movies and tv_show that have the same fields, you can use: `PREFIX 2 "movie:" "tv_show:"` 120 | * `SCHEMA ...`: defines the schema, the fields and their type, to index, as you can see in the command, we are using [TEXT](https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples), [NUMERIC](https://oss.redislabs.com/redisearch/Query_Syntax/#numeric_filters_in_query) and [TAG](https://oss.redislabs.com/redisearch/Query_Syntax/#tag_filters), and [SORTABLE](https://oss.redislabs.com/redisearch/Sorting/) parameters. 121 | 122 | You can find information about the [FT.CREATE](https://oss.redislabs.com/redisearch/Commands/#ftcreate) command in the [documentation](https://oss.redislabs.com/redisearch/Commands/#ftcreate). 123 | 124 | 125 | You can look at the index information with the following command: 126 | 127 | ``` 128 | > FT.INFO idx:movie 129 | ``` 130 | 131 | --- 132 | Next: [Query Data](004-query-data.md) 133 | -------------------------------------------------------------------------------- /docs/004-query-data.md: -------------------------------------------------------------------------------- 1 | # Query Data 2 | 3 | The database contains a few movies, and an index, it is now possible to execute some queries. 4 | 5 | ## Queries 6 | 7 | **Example : *All the movies that contains the string "`war`"*** 8 | 9 | ``` 10 | > FT.SEARCH idx:movie "war" 11 | 12 | 1) (integer) 2 13 | 2) "movie:11005" 14 | 3) 1) "title" 15 | 2) "Star Wars: Episode VI - Return of the Jedi" 16 | ... 17 | 14) "tt0086190" 18 | 4) "movie:11002" 19 | 5) 1) "title" 20 | 2) "Star Wars: Episode V - The Empire Strikes Back" 21 | ... 22 | 13) "imdb_id" 23 | 14) "tt0080684" 24 | 25 | ``` 26 | 27 | The FT.SEARCH commands returns a list of results starting with the number of results, then the list of elements (keys & fields). 28 | 29 | As you can see the movie *Star Wars: Episode V - The Empire Strikes Back* is found, even though you used only the word “war” to match “Wars” in the title. This is because the title has been indexed as text, so the field is [tokenized](https://oss.redislabs.com/redisearch/Escaping/) and [stemmed](https://oss.redislabs.com/redisearch/Stemming/). 30 | 31 | Later when looking at the query syntax in more detail you will learn more about the search capabilities. 32 | 33 | It is also possible to limit the list of fields returned by the query using the `RETURN` parameter, let's run the same query, and return only the title and release_year: 34 | 35 | ``` 36 | > FT.SEARCH idx:movie "war" RETURN 2 title release_year 37 | 38 | 1) (integer) 2 39 | 2) "movie:11005" 40 | 3) 1) "title" 41 | 2) "Star Wars: Episode VI - Return of the Jedi" 42 | 3) "release_year" 43 | 4) "1983" 44 | 4) "movie:11002" 45 | 5) 1) "title" 46 | 2) "Star Wars: Episode V - The Empire Strikes Back" 47 | 3) "release_year" 48 | 4) "1980" 49 | ``` 50 | 51 | This query does not specify any "field" and still returns some movies, this is because RediSearch will search all TEXT fields by default. In the current index only the title is present as a TEXT field. You will see later how to update an index, to add more fields to it. 52 | 53 | If you need to perform a query on a specific field you can specify it using the `@field:` syntax, for example: 54 | 55 | ``` 56 | > FT.SEARCH idx:movie "@title:war" RETURN 2 title release_year 57 | ``` 58 | 59 | --- 60 | **Example : *All the movies that contains the string "`war` but NOT the `jedi` one"*** 61 | 62 | Adding the string `-jedi` (minus) will ask the query engine not to return values that contain `jedi`. 63 | 64 | ``` 65 | > FT.SEARCH idx:movie "war -jedi" RETURN 2 title release_year 66 | 67 | 1) (integer) 1 68 | 2) "movie:11002" 69 | 3) 1) "title" 70 | 2) "Star Wars: Episode V - The Empire Strikes Back" 71 | 3) "release_year" 72 | 4) "1980" 73 | ``` 74 | 75 | --- 76 | **Example : *All the movies that contains the string "`gdfather` using fuzzy search"*** 77 | 78 | As you can see the word godfather contains a speelling error, it can however be matched using [fuzzy matching](https://oss.redislabs.com/redisearch/Query_Syntax/#fuzzy_matching). Fuzzy matches are performed based on [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) (LD). 79 | 80 | ``` 81 | > FT.SEARCH idx:movie " %gdfather% " RETURN 2 title release_year 82 | 83 | 1) (integer) 1 84 | 2) "movie:11003" 85 | 3) 1) "title" 86 | 2) "The Godfather" 87 | 3) "release_year" 88 | 4) "1972" 89 | ``` 90 | 91 | --- 92 | **Example : *All `Thriller` movies"*** 93 | 94 | The `genre` fields is indexed as a TAG and allows exact match queries. 95 | 96 | The syntax to query a TAG field is @field_name:{value} 97 | 98 | ``` 99 | > FT.SEARCH idx:movie "@genre:{Thriller}" RETURN 2 title release_year 100 | 101 | 1) (integer) 1 102 | 2) "movie:11004" 103 | 3) 1) "title" 104 | 2) "Heat" 105 | 3) "release_year" 106 | 4) "1995" 107 | 108 | ``` 109 | 110 | --- 111 | **Example : *All `Thriller` or `Action` movies"*** 112 | 113 | ``` 114 | > FT.SEARCH idx:movie "@genre:{Thriller|Action}" RETURN 2 title release_year 115 | 116 | 1) (integer) 3 117 | 2) "movie:11004" 118 | 3) 1) "title" 119 | 2) "Heat" 120 | 3) "release_year" 121 | 4) "1995" 122 | 4) "movie:11005" 123 | 5) 1) "title" 124 | 2) "Star Wars: Episode VI - Return of the Jedi" 125 | 3) "release_year" 126 | 4) "1983" 127 | 6) "movie:11002" 128 | 7) 1) "title" 129 | 2) "Star Wars: Episode V - The Empire Strikes Back" 130 | 3) "release_year" 131 | 4) "1980" 132 | ``` 133 | 134 | You can find more information about the Tag filters in [the documentation](https://redis.io/docs/stack/search/reference/query_syntax/#tag-filters). 135 | 136 | --- 137 | **Example : *All `Thriller` or `Action` movies that does not have `Jedi` in the title"*** 138 | 139 | ``` 140 | > FT.SEARCH idx:movie "@genre:{Thriller|Action} @title:-jedi" RETURN 2 title release_year 141 | 142 | 1) (integer) 2 143 | 2) "movie:11004" 144 | 3) 1) "title" 145 | 2) "Heat" 146 | 3) "release_year" 147 | 4) "1995" 148 | 4) "movie:11002" 149 | 5) 1) "title" 150 | 2) "Star Wars: Episode V - The Empire Strikes Back" 151 | 3) "release_year" 152 | 4) "1980" 153 | ``` 154 | 155 | --- 156 | **Example : *All the movies released between 1970 and 1980 (included)*** 157 | 158 | The FT.SEARCH syntax has two ways to query numeric fields: 159 | 160 | * using the `FILTER` parameter 161 | 162 | or 163 | 164 | * using the `@field` in the query string. 165 | 166 | 167 | ``` 168 | > FT.SEARCH idx:movie * FILTER release_year 1970 1980 RETURN 2 title release_year 169 | ``` 170 | 171 | ``` 172 | > FT.SEARCH idx:movie "@release_year:[1970 1980]" RETURN 2 title release_year 173 | 174 | 1) (integer) 2 175 | 2) "movie:11003" 176 | 3) 1) "title" 177 | 2) "The Godfather" 178 | 3) "release_year" 179 | 4) "1972" 180 | 4) "movie:11002" 181 | 5) 1) "title" 182 | 2) "Star Wars: Episode V - The Empire Strikes Back" 183 | 3) "release_year" 184 | 4) "1980" 185 | 186 | ``` 187 | 188 | To exclude a value prepend it with `(` in the FILTER or query string, for example to exclude 1980: 189 | 190 | ``` 191 | > FT.SEARCH idx:movie "@release_year:[1970 (1980]" RETURN 2 title release_year 192 | ``` 193 | 194 | --- 195 | ## Insert, Update, Delete and Expire Documents 196 | 197 | As part of this tutorial you have: 198 | 199 | 1. Created few movies, as Redis hashes (*that we call document*) with the following key pattern `movie:*` 200 | 2. Created an index using the `FT.CREATE` command 201 | 3. Queried the data using `FT.SEARCH` 202 | 203 | When creating the index, using the `idx:movie ON hash PREFIX 1 "movie:"` parameter you are asking the indexing engine to look at all existing keys and index them. 204 | 205 | Also new information that matches this pattern/type, will be indexed. 206 | 207 | Let's count the number of movies, add a new one, and count again: 208 | 209 | ``` 210 | > FT.SEARCH idx:movie "*" LIMIT 0 0 211 | 212 | 1) (integer) 4 213 | 214 | 215 | > HSET movie:11033 title "Tomorrow Never Dies" plot "James Bond sets out to stop a media mogul's plan to induce war between China and the U.K in order to obtain exclusive global media coverage." release_year 1997 genre "Action" rating 6.5 votes 177732 imdb_id tt0120347 216 | 217 | > FT.SEARCH idx:movie "*" LIMIT 0 0 218 | 219 | 1) (integer) 5 220 | 221 | ``` 222 | 223 | The new movie has been indexed. You can also search on any of the indexed fields: 224 | 225 | ``` 226 | > FT.SEARCH idx:movie "never" RETURN 2 title release_year 227 | 228 | 1) (integer) 1 229 | 2) "movie:11033" 230 | 3) 1) "title" 231 | 2) "Tomorrow Never Dies" 232 | 3) "release_year" 233 | 4) "1997" 234 | ``` 235 | 236 | Now you **update** one of the field, and search for `007` 237 | 238 | ``` 239 | > HSET movie:11033 title "Tomorrow Never Dies - 007" 240 | 241 | 242 | > FT.SEARCH idx:movie "007" RETURN 2 title release_year 243 | 244 | 1) (integer) 1 245 | 2) "movie:11033" 246 | 3) 1) "title" 247 | 2) "Tomorrow Never Dies - 007" 248 | 3) "release_year" 249 | 4) "1997" 250 | ``` 251 | 252 | When you *delete* the hash, the index is also updated, and the same happens when the key expires (TTL-Time To Live). 253 | 254 | For example, set the James Bond movie to expire in 20 seconds time: 255 | 256 | ``` 257 | > EXPIRE "movie:11033" 20 258 | 259 | ``` 260 | 261 | You can run the following query, and you will that the document expires after 20 seconds and the search query will not return any results, showing that the index has been updated. 262 | 263 | ``` 264 | > FT.SEARCH idx:movie "007" RETURN 2 title release_year 265 | 266 | 1) (integer) 267 | 268 | ``` 269 | 270 | > Note: When you are using Redis as your primary database you are not necessarily using TTLs to delete records. However, if the data you are storing and indexing are transient, for example a caching layer at the top of another datastore or Web service, query user sessions content, ... This is often qualified as a "[Ephemeral Search](https://redislabs.com/blog/the-case-for-ephemeral-search/)" use case: lightweight, fast and expiration. 271 | 272 | --- 273 | ## More 274 | You have many additional features regarding indexing and searching that you can find in the documentation: 275 | 276 | * [FT.SEARCH command](https://redis.io/commands/ft.search) 277 | * [Query Syntax](https://redis.io/docs/stack/search/reference/query_syntax/) 278 | 279 | 280 | Let's see how to inspect, modify and drop an index. 281 | 282 | --- 283 | Next: [Manage Indexes](005-manage-index.md) 284 | -------------------------------------------------------------------------------- /docs/005-manage-index.md: -------------------------------------------------------------------------------- 1 | # Manage Index 2 | 3 | 4 | ### Listing and inspecting the indexes 5 | 6 | The `FT._LIST` command provides a list of all RediSearch indexes in your database: 7 | 8 | ``` 9 | > FT._LIST 10 | 1) "idx:movie" 11 | ``` 12 | 13 | `FT.INFO` provides information about a specific index: 14 | 15 | ``` 16 | > FT.INFO "idx:movie" 17 | 18 | 1) "index_name" 19 | 2) "idx:movie" 20 | ... 21 | 5) "index_definition" 22 | ... 23 | 7) "fields" 24 | ... 25 | 9) "num_docs" 26 | 10) "4" 27 | ... 28 | 29 | ``` 30 | 31 | 32 | ### Updating your Indexing 33 | 34 | As you build your application and add more information to the database you may need to add new fields to the index. The `FT.ALTER` command enables you to do this. 35 | 36 | ``` 37 | > FT.ALTER idx:movie SCHEMA ADD plot TEXT WEIGHT 0.5 38 | "OK" 39 | ``` 40 | 41 | The `WEIGHT` declares the importance of this field when calculating result accuracy. This is a multiplication factor (default is 1); so in this example the plot is less important than the title. 42 | 43 | Let's do a query with the new indexed field: 44 | 45 | ``` 46 | > FT.SEARCH idx:movie "empire @genre:{Action}" RETURN 2 title plot 47 | 48 | ``` 49 | 50 | 51 | ### Dropping the Index 52 | 53 | 54 | You can drop an index using the `FT.DROPINDEX` command. 55 | 56 | ``` 57 | > FT.DROPINDEX idx:movie 58 | 59 | "OK" 60 | ``` 61 | 62 | Dropping an index does not impact the indexed hashes, this means that the movies are still inside the database. 63 | 64 | ``` 65 | >SCAN 0 MATCH movie:* 66 | 67 | 1) "0" 68 | 2) 1) "movie:11002" 69 | 2) "movie:11004" 70 | 3) "movie:11003" 71 | 4) "movie:11005" 72 | ``` 73 | 74 | 75 | *Note: You can delete the indexed document/hashes by adding the `DD` parameter.* 76 | 77 | 78 | --- 79 | Next: [Import Sample Dataset](006-import-dataset.md) 80 | -------------------------------------------------------------------------------- /docs/006-import-dataset.md: -------------------------------------------------------------------------------- 1 | # Sample Dataset 2 | 3 | In the previous steps you used only a few movies, let's now import: 4 | 5 | * More movies *to discover more queries*. 6 | * Theaters *to discover the geospatial capabilities*. 7 | * Users *to do some aggregations*. 8 | 9 | ## Dataset Description 10 | 11 | **Movies** 12 | 13 | The file `sample-app/redisearch-docker/dataset/import_movies.redis` is a script that creates 922 Hashes. 14 | 15 | The movie hashes contain the following fields. 16 | 17 | * **`movie:id`** : The unique ID of the movie, internal to this database (used as the key of the hash) 18 | * **`title`** : The title of the movie. 19 | * **`plot`** : A summary of the movie. 20 | * **`genre`** : The genre of the movie, for now a movie will only have a single genre. 21 | * **`release_year`** : The year the movie was released as a numerical value. 22 | * **`rating`** : A numeric value representing the public's rating for this movie. 23 | * **`votes`** : Number of votes. 24 | * **`poster`** : Link to the movie poster. 25 | * **`imdb_id`** : id of the movie in the [IMDB](https://imdb.com) database. 26 | 27 |
28 | Sample Data: movie:343 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 84 | 85 | 86 |
FieldValue
title 40 | Spider-Man 41 |
plot 46 | When bitten by a genetically modified spider a nerdy shy and awkward high school student gains spider-like abilities that he eventually must use to fight evil as a superhero after tragedy befalls his family. 47 |
genre 52 | Action 53 |
release_year 58 | 2002 59 |
rating 64 | 7.3 65 |
votes 70 | 662219 71 |
poster 76 | https://m.media-amazon.com/images/M/MV5BZDEyN2NhMjgtMjdhNi00MmNlLWE5YTgtZGE4MzNjMTRlMGEwXkEyXkFqcGdeQXVyNDUyOTg3Njg@._V1_SX300.jpg 77 |
imdb_id 82 | tt0145487 83 |
87 |
88 | 89 | **Theaters** 90 | 91 | The file `sample-app/redisearch-docker/dataset/import_theaters.redis` is a script that creates 117 Hashes (used for Geospatial queries). *This dataset is a list of New York Theaters, and not movie theaters, but it is not that critical for this project ;).* 92 | 93 | The theater hashes contain the following fields. 94 | 95 | * **`theater:id`** : The unique ID of the theater, internal to this database (used as the key of the hash) 96 | * **`name`** : The name of the theater 97 | * **`address`** : The street address 98 | * **`city`** : The city, in this sample dataset all the theaters are in New York 99 | * **`zip`** : The zip code 100 | * **`phone`** : The phone number 101 | * **`url`** : The URL of the theater 102 | * **`location`** : Contains the `longitude,latitude` used to create the Geo-indexed field 103 | 104 | 105 |
106 | Sample Data: theater:20 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 132 | 133 | 134 | 135 | 138 | 139 | 140 | 141 | 144 | 145 | 146 | 147 | 150 | 151 | 152 | 153 | 156 | 157 | 158 |
FieldValue
name 118 | Broadway Theatre 119 |
address 124 | 1681 Broadway 125 |
city 130 | New York 131 |
zip 136 | 10019 137 |
phone 142 | 212 944-3700 143 |
url 148 | http://www.shubertorganization.com/theatres/broadway.asp 149 |
location 154 | -73.98335054631019,40.763270202723625 155 |
159 |
160 | 161 | 162 | **Users** 163 | 164 | The file `sample-app/redisearch-docker/dataset/import_users.redis` is a script that creates 5996 Hashes. 165 | 166 | The user hashes contain the following fields. 167 | 168 | * **`user:id`** : The unique ID of the user. 169 | * **`first_name`** : The first name of the user. 170 | * **`last_name`** : The last name of the user. 171 | * **`email`** : The email of the user. 172 | * **`gender`** : The gender of the user (`female`/`male`). 173 | * **`country`** : The country name of the user. 174 | * **`country_code`** : The country code of the user. 175 | * **`city`** : The city of the user. 176 | * **`longitude`** : The longitude of the user. 177 | * **`latitude`** : The latitude of the user. 178 | * **`last_login`** : The last login time for the user, as EPOC time. 179 | * **`ip_address`** : The IP address of the user. 180 | 181 |
182 | Sample Data: user:3233 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 196 | 197 | 198 | 199 | 202 | 203 | 204 | 205 | 208 | 209 | 210 | 211 | 214 | 215 | 216 | 217 | 220 | 221 | 222 | 223 | 226 | 227 | 228 | 229 | 232 | 233 | 234 | 235 | 238 | 239 | 240 | 241 | 244 | 245 | 246 | 247 | 250 | 251 | 252 | 253 | 256 | 257 | 258 |
FieldValue
first_name 194 | Rosetta 195 |
last_name 200 | Olyff 201 |
email 206 | rolyff6g@163.com 207 |
gender 212 | female 213 |
country 218 | China 219 |
country_code 224 | CN 225 |
city 230 | Huangdao 231 |
longitude 236 | 120.04619 237 |
latitude 242 | 35.872664 243 |
last_login 248 | 1570386621 249 |
ip_address 254 | 218.47.90.79 255 |
259 |
260 | 261 | 262 | --- 263 | 264 | ## Importing the Movies, Theaters and Users 265 | 266 | Before importing the data, flush the database: 267 | 268 | ``` 269 | > FLUSHALL 270 | ``` 271 | 272 | 273 | The easiest way to import the file is to use the `redis-cli`, using the following terminal command: 274 | 275 | ``` 276 | $ redis-cli -h localhost -p 6379 < ./sample-app/redisearch-docker/dataset/import_movies.redis 277 | 278 | $ redis-cli -h localhost -p 6379 < ./sample-app/redisearch-docker/dataset/import_theaters.redis 279 | 280 | 281 | $ redis-cli -h localhost -p 6379 < ./sample-app/redisearch-docker/dataset/import_users.redis 282 | 283 | ``` 284 | 285 | 286 | Using Redis Insight or the `redis-cli` you can look at the dataset: 287 | 288 | ``` 289 | > HMGET "movie:343" title release_year genre 290 | 291 | 1) "Spider-Man" 292 | 2) "2002" 293 | 3) "Action" 294 | 295 | 296 | > HMGET "theater:20" name location 297 | 1) "Broadway Theatre" 298 | 2) "-73.98335054631019,40.763270202723625" 299 | 300 | 301 | 302 | > HMGET "user:343" first_name last_name last_login 303 | 1) "Umeko" 304 | 2) "Castagno" 305 | 3) "1574769122" 306 | 307 | ``` 308 | 309 | You can also use the `DBSIZE` command to see how many keys you have in your database. 310 | 311 | --- 312 | 313 | ## Create Indexes 314 | 315 | 316 | **Create the `idx:movie` index:** 317 | 318 | ``` 319 | > FT.CREATE idx:movie ON hash PREFIX 1 "movie:" SCHEMA title TEXT SORTABLE plot TEXT WEIGHT 0.5 release_year NUMERIC SORTABLE rating NUMERIC SORTABLE votes NUMERIC SORTABLE genre TAG SORTABLE 320 | 321 | "OK" 322 | ``` 323 | 324 | The movies have now been indexed, you can run the `FT.INFO "idx:movie"` command and look at the `num_docs` returned value. (should be 922). 325 | 326 | **Create the `idx:theater` index:** 327 | 328 | This index will mostly be used to show the geospatial capabilties of RediSearch. 329 | 330 | In the previous examples we have created indexes with 3 types: 331 | 332 | * `Text` 333 | * `Numeric` 334 | * `Tag` 335 | 336 | You will now discover a new type of field: `Geo`. 337 | 338 | The `theater` hashes contains a field `location` with the longitude and latitude, that will be used in the index as follows: 339 | 340 | ``` 341 | > FT.CREATE idx:theater ON hash PREFIX 1 "theater:" SCHEMA name TEXT SORTABLE location GEO 342 | 343 | "OK" 344 | ``` 345 | 346 | The theaters have been indexed, you can run the `FT.INFO "idx:theater"` command and look at the `num_docs` returned value. (should be 117). 347 | 348 | 349 | **Create the `idx:user` index:** 350 | 351 | 352 | ``` 353 | > FT.CREATE idx:user ON hash PREFIX 1 "user:" SCHEMA gender TAG country TAG SORTABLE last_login NUMERIC SORTABLE location GEO 354 | 355 | "OK" 356 | ``` 357 | 358 | 359 | --- 360 | Next: [Querying the movie database](007-query-movies.md) 361 | -------------------------------------------------------------------------------- /docs/007-query-movies.md: -------------------------------------------------------------------------------- 1 | # Querying the Movie Dataset 2 | 3 | 4 | 5 | As described earlier in the tutorial, one of the goals of RediSearch is to provide rich querying capabilities such as: 6 | 7 | * simple and complex conditions 8 | * sorting 9 | * pagination 10 | * counting 11 | 12 | 13 | ### Conditions 14 | 15 | The best way to start to work with RediSearch query capabilities is to look at the various conditions options. 16 | 17 | 18 |
19 | 20 | 21 | Find all the movies that contain the word 'heat' or related to 'heat' 22 | 23 | 24 | 25 | ``` 26 | > FT.SEARCH "idx:movie" "heat" RETURN 2 title plot 27 | 28 | 1) (integer) 4 29 | 2) "movie:1141" 30 | 3) 1) "title" 31 | 2) "Heat" 32 | 3) "plot" 33 | 4) "A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist." 34 | 4) "movie:818" 35 | 5) 1) "title" 36 | 2) "California Heat" 37 | 3) "plot" 38 | 4) "A lifeguard bets he can be true to just one woman." 39 | 6) "movie:736" 40 | 7) 1) "title" 41 | 2) "Chicago Justice" 42 | 3) "plot" 43 | 4) "The State's Attorney's dedicated team of prosecutors and investigators navigates heated city politics and controversy head-on,while fearlessly pursuing justice." 44 | 8) "movie:1109" 45 | 9) 1) "title" 46 | 2) "Love & Hip Hop: Miami" 47 | 3) "plot" 48 | 4) "'Love and Hip Hop Miami' turns up the heat and doesn't hold back in making the 305 the place to be. Multi-platinum selling hip-hop legend Trick Daddy is back in the studio collaborating ..." 49 | 50 | ``` 51 | 52 | The first line contains the number of documents (`4`) that match the query condition, then the list of movies. 53 | 54 | This query is a "fieldless" condition, this means that the query engine has: 55 | * searched in all the TEXT fields of the index(`title` and `plot`) 56 | * for the word `heat` and related words, this is why the movie:736 is returned since it has the word `heated` in the plot ([stemming](https://oss.redislabs.com/redisearch/Stemming/)) 57 | * returned the result sorted by score, remember that the title has a weight of 1.0, and the plot a weight of 0.5. So when the word or related words are found in the title the score is larger. 58 | --- 59 |
60 | 61 | 62 | 63 |
64 | 65 | 66 | Find all the movies with a title that contains the word 'heat' or related to 'heat' 67 | 68 | 69 | 70 | In this case you have to set the criteria to a the field title using the `@title` notation. 71 | 72 | ``` 73 | > FT.SEARCH "idx:movie" "@title:heat" RETURN 2 title plot 74 | 1) (integer) 2 75 | 2) "movie:1141" 76 | 3) 1) "title" 77 | 2) "Heat" 78 | 3) "plot" 79 | 4) "A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist." 80 | 4) "movie:818" 81 | 5) 1) "title" 82 | 2) "California Heat" 83 | 3) "plot" 84 | 4) "A lifeguard bets he can be true to just one woman." 85 | 86 | ``` 87 | 88 | So only 2 movies are returned. 89 | 90 | --- 91 |
92 | 93 | 94 |
95 | 96 | 97 | Find all the movies where the title contains 'heat' and does NOT contains 'california' 98 | 99 | 100 | 101 | For this you add parentheses around the field condition and add the `-` sign to 'california'. 102 | 103 | ``` 104 | > FT.SEARCH "idx:movie" "@title:(heat -california)" RETURN 2 title plot 105 | 1) (integer) 1 106 | 2) "movie:1141" 107 | 3) 1) "title" 108 | 2) "Heat" 109 | 3) "plot" 110 | 4) "A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist." 111 | 112 | ``` 113 | 114 | Only one movie is returned. 115 | 116 | If you do not put the `( .. )` the `-california` condition will be applied to all the text fields. 117 | 118 | You can do test this with the following queries: 119 | 120 | ``` 121 | > FT.SEARCH "idx:movie" "@title:(heat -woman)" RETURN 2 title plot 122 | ``` 123 | 124 | ``` 125 | > FT.SEARCH "idx:movie" "@title:heat -woman" RETURN 2 title plot 126 | ``` 127 | 128 | As you can see the first query only searches for woman in the title and returns two movies "Heat" and "California Heat", where the second query eliminates "California Heat" from the list since the plot contains the word `woman`. 129 | 130 | --- 131 |
132 | 133 | 134 | 135 |
136 | 137 | 138 | Find all the 'Drama' movies that have 'heat' in the title 139 | 140 | 141 | 142 | As you have seen earlier the movie index contains: 143 | * the `title` and plot as TEXT 144 | * the `genre` as TAG. 145 | 146 | You saw earlier how to place a condition on a TEXT field. 147 | 148 | The [TAG](https://oss.redislabs.com/redisearch/Tags/) is a little bit different as the index engine does not do any stemming. 149 | 150 | To set a condition on this field you must use the `@field:{value}` notation, the `{...}` indicates that it is a TAG condition 151 | 152 | 153 | ``` 154 | > FT.SEARCH "idx:movie" "@title:(heat) @genre:{Drama} " RETURN 3 title plot genre 155 | 1) (integer) 1 156 | 2) "movie:1141" 157 | 3) 1) "title" 158 | 2) "Heat" 159 | 3) "plot" 160 | 4) "A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist." 161 | 5) "genre" 162 | 6) "Drama" 163 | ``` 164 | 165 | As you can see this query applies conditions to two different fields with an exact match on the TAG. 166 | 167 | TAG is the structure to use when you want to do exact matches on strings/words. 168 | --- 169 |
170 | 171 | 172 |
173 | 174 | 175 | Find all the 'Drama' or 'Comedy' movies that have 'heat' in the title 176 | 177 | 178 | 179 | This is similar to the previous query, you can pass a list of values with the `|` to represent the OR. 180 | 181 | 182 | ``` 183 | > FT.SEARCH "idx:movie" "@title:(heat) @genre:{Drama|Comedy} " RETURN 3 title plot genre 184 | 185 | 1) (integer) 2 186 | 2) "movie:1141" 187 | 3) 1) "title" 188 | 2) "Heat" 189 | 3) "plot" 190 | 4) "A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist." 191 | 5) "genre" 192 | 6) "Drama" 193 | 4) "movie:818" 194 | 5) 1) "title" 195 | 2) "California Heat" 196 | 3) "plot" 197 | 4) "A lifeguard bets he can be true to just one woman." 198 | 5) "genre" 199 | 6) "Comedy" 200 | ``` 201 | 202 | 203 | You can also put the '|' between all the conditions to search for example all movies that have "heat" in the title, or that are Comedy or that are Drama. The query will look like: 204 | 205 | ``` 206 | FT.SEARCH "idx:movie" "@title:(heat) | @genre:{Drama|Comedy} " RETURN 3 title plot genre 207 | ``` 208 | 209 | --- 210 |
211 | 212 | 213 |
214 | 215 | Find all 'Mystery' OR 'Thriller' movies, released in 2014 OR 2018 216 | 217 | 218 | In this query, the new item is the query on a numeric field (release_year). 219 | 220 | Like before, for the condition you have to use the `@field:` notation, but for a numeric field you have to put the interval of the condition. 221 | 222 | In this query it will be two conditions with an OR (`|`). 223 | 224 | ``` 225 | > FT.SEARCH "idx:movie" "@genre:{Mystery|Thriller} (@release_year:[2018 2018] | @release_year:[2014 2014] )" RETURN 3 title release_year genre 226 | 227 | 1) (integer) 3 228 | 2) "movie:461" 229 | 3) 1) "title" 230 | 2) "The Boat ()" 231 | 3) "release_year" 232 | 4) "2018" 233 | 5) "genre" 234 | 6) "Mystery" 235 | 4) "movie:65" 236 | 5) 1) "title" 237 | 2) "The Loft" 238 | 3) "release_year" 239 | 4) "2014" 240 | 5) "genre" 241 | 6) "Mystery" 242 | 6) "movie:989" 243 | 7) 1) "title" 244 | 2) "Los Angeles Overnight" 245 | 3) "release_year" 246 | 4) "2018" 247 | 5) "genre" 248 | 6) "Thriller" 249 | ``` 250 | 251 | 252 | --- 253 |
254 | 255 | Summary 256 | 257 | * Fieldless queries apply to all TEXT fields and use the words and their base form (stemming) 258 | * To apply a condition to a specific field you must use the `@field:` notation 259 | * Multiple conditions are "intersection" (AND condition), to do a "union" (OR condition), you have to use the "`|`" character. 260 | 261 | ---- 262 | ### Sort 263 | 264 | A very common use case when querying data is to sort the data on a specific field, and paginate over the result. 265 | 266 |
267 | 268 | Query all the `Action` movies, sorted by release year from most recent to the oldest 269 | 270 | 271 | ``` 272 | > FT.SEARCH "idx:movie" "@genre:{Action}" SORTBY release_year DESC RETURN 2 title release_year 273 | 1) (integer) 186 274 | 2) "movie:360" 275 | 3) 1) "release_year" 276 | 2) "2019" 277 | 3) "title" 278 | 4) "Spider-Man: Far from Home" 279 | ... 280 | 20) "movie:278" 281 | 21) 1) "release_year" 282 | 2) "2016" 283 | 3) "title" 284 | 4) "Mechanic: Resurrection" 285 | ``` 286 | 287 | The first line contains the number of documents (`186`) that match the query condition. 288 | 289 | The FT.SEARCH command, by default, returns the first ten documents. You will see in the next query how to paginate. 290 | 291 | You can only use one SORTBY clause in an FT.SEARCH query, if you want to sort on multiple fields, for example sorting movies by `genre` ascending and `release_year` descending, you have to use an FT.AGGREGATE, this is covered in the [next section](008-aggregation.md). 292 | 293 | Note: The field used in the [SORTBY](https://oss.redislabs.com/redisearch/Sorting/#specifying_sortby) should be part of the index schema and defined as SORTABLE. 294 | --- 295 |
296 | 297 | ---- 298 | ### Paginate 299 | 300 |
301 | 302 | Query all the `Action` movies, sorted by release year from the oldest to the most recent one, returning the record by batch of 100 movies 303 | 304 | 305 | ``` 306 | > FT.SEARCH "idx:movie" "@genre:{Action}" LIMIT 0 100 SORTBY release_year ASC RETURN 2 title release_year 307 | 1) (integer) 186 308 | 2) "movie:892" 309 | 3) 1) "release_year" 310 | 2) "1966" 311 | 3) "title" 312 | 4) "Texas,Adios" 313 | ... 314 | 200) "movie:12" 315 | 201) 1) "release_year" 316 | 2) "2014" 317 | 3) "title" 318 | 4) "Fury" 319 | ``` 320 | 321 | The result is very similar to the previous query: 322 | * 186 documents found 323 | * the first document is the oldest one, released in 1966 324 | * the latest movie of the batch was released in 2014 325 | 326 | 327 | To paginate to the next batch you need to change the limit as follows: 328 | 329 | ``` 330 | > FT.SEARCH "idx:movie" "@genre:{Action}" LIMIT 100 200 SORTBY release_year ASC RETURN 2 title release_year 331 | ``` 332 | --- 333 |
334 | 335 | 336 | ---- 337 | ### Count 338 | 339 | 340 |
341 | 342 | Count the number of 'Action' movies 343 | 344 | 345 | Based on the sample queries that you have seen earlier, if you specify `LIMIT 0 0` it will give you the number of documents based on the query condition. 346 | 347 | ``` 348 | > FT.SEARCH "idx:movie" "@genre:{Action}" LIMIT 0 0 349 | 350 | 1) (integer) 186 351 | ``` 352 | --- 353 |
354 | 355 | 356 |
357 | 358 | Count the number of 'Action' movies released in 2017 359 | 360 | 361 | Based on the sample queries that you have seen earlier, if you specify `LIMIT 0 0` it will give you the number of documents based on the query condition. 362 | 363 | ``` 364 | > FT.SEARCH "idx:movie" "@genre:{Action}" FILTER release_year 2017 2017 LIMIT 0 0 365 | 366 | 1) (integer) 5 367 | ``` 368 | 369 | You can also use the following syntax: 370 | 371 | ``` 372 | > FT.SEARCH "idx:movie" "@genre:{Action} @release_year:[2017 2017]" LIMIT 0 0 373 | 374 | 1) (integer) 5 375 | ``` 376 | 377 | --- 378 |
379 | 380 | 381 | ---- 382 | ### Geospatial Queries 383 | 384 |
385 | 386 | Find theaters, name and address, that are at less than 400 meters from MOMA 387 | 388 | 389 | Suppose you are at the MOMA, located at "11 W 53rd St, New York", and you want to find all the theaters located in a 400m radius. 390 | 391 | For this you need to determine the lat/long position of your current location `-73.9798156,40.7614367`, and execute the following query: 392 | 393 | ``` 394 | > FT.SEARCH "idx:theater" "@location:[-73.9798156 40.7614367 400 m]" RETURN 2 name address 395 | 396 | 1) (integer) 5 397 | 2) "theater:30" 398 | 3) 1) "name" 399 | 2) "Ed Sullivan Theater" 400 | 3) "address" 401 | 4) "1697 Broadway" 402 | ... 403 | 10) "theater:115" 404 | 11) 1) "name" 405 | 2) "Winter Garden Theatre" 406 | 3) "address" 407 | 4) "1634 Broadway" 408 | ``` 409 | 410 | --- 411 |
412 | 413 | 414 | 415 | ---- 416 | Next: [Aggregation](008-aggregation.md) 417 | -------------------------------------------------------------------------------- /docs/008-aggregation.md: -------------------------------------------------------------------------------- 1 | # Aggregation 2 | 3 | A common need for applications, in addition to retrieving information as a document list, like you have done with the "`FT.SEARCH`" command, is to do some "aggregation". 4 | 5 | For example if we look at the movie documents, you may want to retrieve the number of movies grouped by release year starting with the most recent ones. 6 | 7 | For this, RediSearch provides the FT.AGGREGATE command, with aggregations described as a data processing pipeline. 8 | 9 | Let's check out some examples. 10 | 11 | ## Group By & Sort By 12 | 13 |
14 | 15 | 16 | Number of movies by year 17 | 18 | 19 | 20 | ``` 21 | > FT.AGGREGATE "idx:movie" "*" GROUPBY 1 @release_year REDUCE COUNT 0 AS nb_of_movies 22 | 23 | 1) (integer) 60 24 | 2) 1) "release_year" 25 | 2) "1964" 26 | 3) "nb_of_movies" 27 | 4) "9" 28 | ... 29 | 61) 1) "release_year" 30 | 2) "2010" 31 | 3) "nb_of_movies" 32 | 4) "15" 33 | ``` 34 | 35 | 36 | 37 | --- 38 |
39 | 40 | 41 |
42 | 43 | 44 | Number of movies by year from the most recent to the oldest 45 | 46 | 47 | 48 | ``` 49 | > FT.AGGREGATE "idx:movie" "*" GROUPBY 1 @release_year REDUCE COUNT 0 AS nb_of_movies SORTBY 2 @release_year DESC 50 | 51 | 1) (integer) 60 52 | 2) 1) "release_year" 53 | 2) "2019" 54 | 3) "nb_of_movies" 55 | 4) "14" 56 | ... 57 | 11) 1) "release_year" 58 | 2) "2010" 59 | 3) "nb_of_movies" 60 | 4) "15" 61 | ``` 62 | --- 63 |
64 | 65 |
66 | 67 | 68 | Number of movies by genre, with the total number of votes, and average rating 69 | 70 | 71 | 72 | ``` 73 | > FT.AGGREGATE idx:movie "*" GROUPBY 1 @genre REDUCE COUNT 0 AS nb_of_movies REDUCE SUM 1 votes AS nb_of_votes REDUCE AVG 1 rating AS avg_rating SORTBY 4 @avg_rating DESC @nb_of_votes DESC 74 | 75 | 76 | 1) (integer) 26 77 | 2) 1) "genre" 78 | 2) "fantasy" 79 | 3) "nb_of_movies" 80 | 4) "1" 81 | 5) "nb_of_votes" 82 | 6) "1500090" 83 | 7) "avg_rating" 84 | 8) "8.8" 85 | ... 86 | 11) 1) "genre" 87 | 2) "romance" 88 | 3) "nb_of_movies" 89 | 4) "2" 90 | 5) "nb_of_votes" 91 | 6) "746" 92 | 7) "avg_rating" 93 | 8) "6.65" 94 | ``` 95 | 96 | --- 97 |
98 | 99 | 100 |
101 | 102 | 103 | Count the number of females by country sorted from the biggest to smallest number. 104 | 105 | 106 | 107 | ``` 108 | > FT.AGGREGATE idx:user "@gender:{female}" GROUPBY 1 @country REDUCE COUNT 0 AS nb_of_users SORTBY 2 @nb_of_users DESC 109 | 110 | 1) (integer) 193 111 | 2) 1) "country" 112 | 2) "china" 113 | 3) "nb_of_users" 114 | 4) "537" 115 | ... 116 | 11) 1) "country" 117 | 2) "ukraine" 118 | 3) "nb_of_users" 119 | 4) "72" 120 | ``` 121 | 122 | --- 123 |
124 | 125 | 126 | ## Apply Functions 127 | 128 | 129 |
130 | 131 | 132 | Number of logins per year and month 133 | 134 | 135 | 136 | The `idx:user` index contains the last_login field. This field stores the last login time as an EPOC timestamp. 137 | 138 | RediSearch aggregation allows you to apply transformations to each record. This is done using the [APPLY](https://oss.redislabs.com/redisearch/Aggregations/#apply_expressions) parameter. 139 | 140 | For this example you have to use a [date/time](https://oss.redislabs.com/redisearch/Aggregations/#list_of_datetime_apply_functions) function to extract the month and year from the timestamp. 141 | 142 | ``` 143 | > FT.AGGREGATE idx:user * APPLY year(@last_login) AS year APPLY "monthofyear(@last_login) + 1" AS month GROUPBY 2 @year @month REDUCE count 0 AS num_login SORTBY 4 @year ASC @month ASC 144 | 145 | 1) (integer) 13 146 | 2) 1) "year" 147 | 2) "2019" 148 | 3) "month" 149 | 4) "9" 150 | 5) "num_login" 151 | 6) "230" 152 | ... 153 | 14) 1) "year" 154 | 2) "2020" 155 | 3) "month" 156 | 4) "9" 157 | 5) "num_login" 158 | 6) "271" 159 | 160 | ``` 161 | 162 | --- 163 |
164 | 165 |
166 | 167 | 168 | Number of logins per weekday 169 | 170 | 171 | 172 | Using the date/time Apply functions it is possible to extract the day of the week from the timestamp, so let's see how the logins are distributed over the week. 173 | 174 | ``` 175 | > FT.AGGREGATE idx:user * APPLY "dayofweek(@last_login) +1" AS dayofweek GROUPBY 1 @dayofweek REDUCE count 0 AS num_login SORTBY 2 @dayofweek ASC 176 | 177 | 1) (integer) 7 178 | 2) 1) "dayofweek" 179 | 2) "1" 180 | 3) "num_login" 181 | 4) "815" 182 | ... 183 | 8) 1) "dayofweek" 184 | 2) "7" 185 | 3) "num_login" 186 | 4) "906" 187 | 188 | ``` 189 | 190 | --- 191 |
192 | 193 | ## Filter 194 | 195 | In the previous example you used the `query string` parameter to select all documents (`"*"`) or a subset of the documents (`"@gender:{female}"`) 196 | 197 | It is also possible to filter the results using a predicate expression relating to values in each result. This is applied post-query and relates to the current state of the pipeline. This is done using the [FILTER](https://oss.redislabs.com/redisearch/Aggregations/#filter_expressions) parameter. 198 | 199 | 200 |
201 | 202 | 203 | Count the number of females by country, except China, with more than 100 users, and sorted from the biggest to lowest number 204 | 205 | 206 | 207 | ``` 208 | > FT.AGGREGATE idx:user "@gender:{female}" GROUPBY 1 @country REDUCE COUNT 0 AS nb_of_users FILTER "@country!='china' && @nb_of_users > 100" SORTBY 2 @nb_of_users DESC 209 | 210 | 1) (integer) 163 211 | 2) 1) "country" 212 | 2) "indonesia" 213 | 3) "nb_of_users" 214 | 4) "309" 215 | ... 216 | 6) 1) "country" 217 | 2) "brazil" 218 | 3) "nb_of_users" 219 | 4) "108" 220 | ``` 221 | 222 | --- 223 |
224 | 225 | 226 |
227 | 228 | 229 | Number of login per month, for year 2020 230 | 231 | 232 | 233 | This is similar to the previous query with the addition of a filter on the year. 234 | 235 | ``` 236 | > FT.AGGREGATE idx:user * APPLY year(@last_login) AS year APPLY "monthofyear(@last_login) + 1" AS month GROUPBY 2 @year @month REDUCE count 0 AS num_login FILTER "@year==2020" SORTBY 2 @month ASC 237 | 238 | 1) (integer) 13 239 | 2) 1) "year" 240 | 2) "2020" 241 | 3) "month" 242 | 4) "1" 243 | 5) "num_login" 244 | 6) "520" 245 | ... 246 | 10) 1) "year" 247 | 2) "2020" 248 | 3) "month" 249 | 4) "9" 250 | 5) "num_login" 251 | 6) "271" 252 | 253 | ``` 254 | 255 | --- 256 |
257 | 258 | 259 | ---- 260 | Next: [Advanced Options](009-advanced-features.md) 261 | -------------------------------------------------------------------------------- /docs/009-advanced-features.md: -------------------------------------------------------------------------------- 1 | # Other Options 2 | 3 | ## Create an index using a Filter 4 | 5 | In the previous examples, the indices were created using a `PREFIX`, where all the keys matching the type and prefix are indexed. 6 | 7 | It is also possible to create an index using a filter, for example create an index with all the "Drama" movies released between 1990 and 2000 (2000 not included). 8 | 9 | The [`FILTER`](https://oss.redislabs.com/redisearch/Aggregations/#filter_expressions)` expression is using the [aggregation filter syntax(https://oss.redislabs.com/redisearch/Aggregations/#filter_expressions)], for example for the genre and release year it will be 10 | 11 | * `FILTER "@genre=='Drama' && @release_year>=1990 && @release_year<2000"` 12 | 13 | So when you create the index: 14 | 15 | `FT.CREATE idx:drama ON Hash PREFIX 1 "movie:" FILTER "@genre=='Drama' && @release_year>=1990 && @release_year<2000" SCHEMA title TEXT SORTABLE release_year NUMERIC SORTABLE ` 16 | 17 | You can run the `FT.INFO idx:drama` command to look at the index definitions and statistics. 18 | 19 | Notes 20 | * The `PREFIX` is not optional. 21 | * In this appliation this index is not useful since you can get the same data from the `idx:movie` 22 | 23 | 24 | You can check that the data has been indexed by running the following queries that should return the same number of documents. 25 | 26 | On `idx:drama` 27 | 28 | ``` 29 | > FT.SEARCH idx:drama " @release_year:[1990 (2000]" LIMIT 0 0 30 | 31 | 1) (integer) 24 32 | ``` 33 | 34 | On `idx"movie` 35 | 36 | ``` 37 | > FT.SEARCH idx:movie "@genre:{Drama} @release_year:[1990 (2000]" LIMIT 0 0 38 | 39 | 1) (integer) 24 40 | ``` 41 | 42 | 43 | 44 | ---- 45 | Next: [Sample Application](010-application-development.md) 46 | 47 | -------------------------------------------------------------------------------- /docs/010-application-development.md: -------------------------------------------------------------------------------- 1 | # Application Development 2 | 3 | It is time now to see how to use RediSearch in your application. 4 | 5 | You can find the same REST Service that uses RediSearch developed with different programming languages. 6 | 7 | * [RediSearch & Java : Jedis/JRediSearch](../../../tree/master/sample-app/redisearch-jedis-rest) 8 | 9 | * [Node.js : Node RediSearch](../../../tree/master/sample-app/redisearch-node-rest) 10 | 11 | * [Python : Python RediSearch](../../../tree/master/sample-app/redisearch-python-rest) 12 | 13 | The frontend is created using a Vue.js application that let you run search queries using each of the REST backend. 14 | 15 | ![Application Architecture](https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/master/docs/images/sample-app-archi.png) 16 | 17 | 18 | ## Run the Sample Application 19 | 20 | The application and all the services, including RediSearch, are available as a Docker Compose application. 21 | 22 | 23 | If you have not already downloaded the project, clone it: 24 | 25 | ``` 26 | > git clone https://github.com/RediSearch/redisearch-getting-started.git 27 | 28 | > cd redisearch-getting-started 29 | ``` 30 | 31 | 32 | To run the application: 33 | 34 | ``` 35 | > cd sample-app 36 | 37 | > docker-compose up --force-recreate --build 38 | 39 | ``` 40 | 41 | This Docker Compose will start: 42 | 43 | 1. A Redis Stack container on port 6379. The redis-cli can be used with this container once the ports are exposed 44 | 1. The Java, Node and Python REST Services available on port 8085, 8086, 8087 45 | 1. The frontend on port 8084 46 | 1. A second RediStack container will start on port 6380 just to load the sample data to the redis stack instance running on port 6379. This container exits once the sample data has been loaded to the 6379 container 47 | 48 | Once started you can access the application and its services using the following URLs: 49 | 50 | * http://localhost:8084 51 | * http://localhost:8085/api/1.0/movies/search?q=star&offset=0&limit=10 52 | * http://localhost:8086/api/1.0/movies/search?q=star&offset=0&limit=10 53 | * http://localhost:8087/api/1.0/movies/search?q=star&offset=0&limit=10 54 | 55 | 56 | 57 | #### Stop and Delete Everything 58 | 59 | Run the following command to delete the containers & images: 60 | 61 | ``` 62 | > docker-compose down -v --rmi local --remove-orphans 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 19 | 26 | 29 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/images/sample-app-archi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/0b5d84954f1ad9557aece8907d1ea303d62bba90/docs/images/sample-app-archi.png -------------------------------------------------------------------------------- /docs/images/secondary-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/0b5d84954f1ad9557aece8907d1ea303d62bba90/docs/images/secondary-index.png -------------------------------------------------------------------------------- /sample-app/README.md: -------------------------------------------------------------------------------- 1 | # Sample Application 2 | 3 | Go to [Sample Application](../docs/010-application-development.md) 4 | -------------------------------------------------------------------------------- /sample-app/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.10' 2 | services: 3 | 4 | # Start RediSearch 5 | redis-stack: 6 | image: redis/redis-stack:latest 7 | container_name: redis-stack 8 | ports: 9 | - "6379:6379" 10 | - "8001:8001" 11 | networks: 12 | - redisearch-gettingstarted 13 | volumes: 14 | - ./redisearch-docker/dataset:/dataset 15 | - ./redis:/data` 16 | 17 | # Start redis-stack to run load script and exit 18 | redis-stack-2: 19 | image: redis/redis-stack:latest 20 | container_name: redis-stack-2 21 | ports: 22 | - "6380:6379" 23 | networks: 24 | - redisearch-gettingstarted 25 | volumes: 26 | - ./redisearch-docker/dataset:/dataset 27 | command: 28 | [ "sh", "./dataset/import-data.sh" ] 29 | depends_on: 30 | - redis-stack 31 | 32 | rest-java: 33 | build: 34 | context: ./redisearch-jedis-rest 35 | dockerfile: Dockerfile 36 | container_name: rest-java 37 | ports: 38 | - "8085:8085" 39 | environment: 40 | - REDIS_URL=redis://redis-stack:6379 41 | - REDIS_INDEX=idx:movie 42 | networks: 43 | - redisearch-gettingstarted 44 | restart: always 45 | depends_on: 46 | - redis-stack 47 | 48 | rest-node: 49 | build: 50 | context: ./redisearch-node-rest 51 | dockerfile: Dockerfile 52 | container_name: rest-node 53 | ports: 54 | - "8086:8086" 55 | environment: 56 | - REDIS_URL=redis://redis-stack:6379 57 | - REDIS_INDEX=idx:movie 58 | networks: 59 | - redisearch-gettingstarted 60 | restart: always 61 | depends_on: 62 | - redis-stack 63 | 64 | 65 | rest-python: 66 | build: 67 | context: ./redisearch-python-rest 68 | dockerfile: Dockerfile 69 | container_name: rest-python 70 | ports: 71 | - "8087:8087" 72 | environment: 73 | - REDIS_SERVER=redis-stack 74 | - REDIS_PORT=6379 75 | - REDIS_INDEX=idx:movie 76 | networks: 77 | - redisearch-gettingstarted 78 | restart: always 79 | depends_on: 80 | - redis-stack 81 | 82 | search-frontend: 83 | build: 84 | context: ./front-end 85 | dockerfile: Dockerfile 86 | container_name: search-frontend 87 | ports: 88 | - "8084:8084" 89 | environment: 90 | - VUE_APP_SEARCH_API_JAVA=http://rest-java:8085 91 | - VUE_APP_SEARCH_API_NODE=http://rest-node:8086 92 | - VUE_APP_SEARCH_API_PYTHON=http://rest-python:8087 93 | networks: 94 | - redisearch-gettingstarted 95 | restart: always 96 | depends_on: 97 | - redis-stack 98 | - rest-java 99 | - rest-node 100 | - rest-python 101 | 102 | networks: 103 | redisearch-gettingstarted: 104 | driver: bridge 105 | -------------------------------------------------------------------------------- /sample-app/front-end/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /sample-app/front-end/.env.development: -------------------------------------------------------------------------------- 1 | 2 | VUE_APP_SEARCH_API_JAVA=http://127.0.0.1:8085 3 | VUE_APP_SEARCH_API_NODE=http://127.0.0.1:8086 4 | VUE_APP_SEARCH_API_PYTHON=http://127.0.0.1:8087 5 | -------------------------------------------------------------------------------- /sample-app/front-end/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample-app/front-end/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /sample-app/front-end/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | # install simple http server for serving static content 4 | RUN npm install -g http-server 5 | 6 | # make the 'app' folder the current working directory 7 | WORKDIR /app 8 | 9 | # copy both 'package.json' and 'package-lock.json' (if available) 10 | COPY package*.json ./ 11 | 12 | # Environment Variables 13 | ARG VUE_APP_SEARCH_API_JAVA 14 | ENV VUE_APP_SEARCH_API_JAVA $VUE_APP_SEARCH_API_JAVA 15 | ARG VUE_APP_SEARCH_API_NODE 16 | ENV VUE_APP_SEARCH_API_NODE $VUE_APP_SEARCH_API_NODE 17 | ARG VUE_APP_SEARCH_API_PYTHON 18 | ENV VUE_APP_SEARCH_API_PYTHON $VUE_APP_SEARCH_API_PYTHON 19 | 20 | # install project dependencies 21 | RUN npm install 22 | 23 | # copy project files and folders to the current working directory (i.e. 'app' folder) 24 | COPY . . 25 | 26 | # build app for production with minification 27 | RUN npm run build 28 | 29 | EXPOSE 8084 30 | CMD [ "http-server", "dist", "--port", "8084" ] 31 | 32 | -------------------------------------------------------------------------------- /sample-app/front-end/README.md: -------------------------------------------------------------------------------- 1 | # front-end 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | 26 | 27 | ### Running the application in Docker 28 | 29 | You can run build and run the application from docker using the following commands: 30 | 31 | **Build** 32 | 33 | ```shell script 34 | 35 | > docker build -t redis/search-frontend . 36 | 37 | ``` 38 | 39 | This command will create a new image and build the maven project into it. 40 | 41 | **Run** 42 | 43 | ```shell script 44 | > docker run --rm \ 45 | --env "VUE_APP_SEARCH_API_JAVA=http://host.docker.internal:8085" \ 46 | --env "VUE_APP_SEARCH_API_NODE=http://host.docker.internal:8086" \ 47 | --env "VUE_APP_SEARCH_API_PYTHON=http://host.docker.internal:8087" \ 48 | --name "redisearch-frontend"\ 49 | -p 8084:8084 redis/search-frontend 50 | ``` 51 | 52 | Access the Web application with the following URL: 53 | 54 | * http://localhost:8084 55 | -------------------------------------------------------------------------------- /sample-app/front-end/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /sample-app/front-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "bootstrap": "^4.5.2", 13 | "bootstrap-vue": "^2.16.0", 14 | "core-js": "^3.6.5", 15 | "vue": "^2.6.11", 16 | "vue-router": "^3.2.0", 17 | "vue-slider-component": "^3.2.5" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.5.0", 21 | "@vue/cli-plugin-eslint": "~4.5.0", 22 | "@vue/cli-plugin-router": "~4.5.0", 23 | "@vue/cli-service": "~4.5.0", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^6.7.2", 26 | "eslint-plugin-vue": "^6.2.2", 27 | "vue-template-compiler": "^2.6.11" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample-app/front-end/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/0b5d84954f1ad9557aece8907d1ea303d62bba90/sample-app/front-end/public/favicon.ico -------------------------------------------------------------------------------- /sample-app/front-end/public/imgs/forkme_left_red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | ]> 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 29 | 33 | 37 | 45 | 50 | 56 | 61 | 67 | 73 | 79 | 84 | 89 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /sample-app/front-end/public/imgs/redis-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-app/front-end/public/imgs/redis.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 48 | 50 | 80 | 82 | 88 | 89 | 90 | 92 | 96 | 98 | 104 | 110 | 116 | 122 | 128 | 134 | 135 | 136 | 141 | 146 | 152 | 157 | 162 | 163 | 169 | 175 | 176 | -------------------------------------------------------------------------------- /sample-app/front-end/public/imgs/redislabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/0b5d84954f1ad9557aece8907d1ea303d62bba90/sample-app/front-end/public/imgs/redislabs.png -------------------------------------------------------------------------------- /sample-app/front-end/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample-app/front-end/src/App.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 53 | 54 | -------------------------------------------------------------------------------- /sample-app/front-end/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/0b5d84954f1ad9557aece8907d1ea303d62bba90/sample-app/front-end/src/assets/logo.png -------------------------------------------------------------------------------- /sample-app/front-end/src/components/Comments.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | -------------------------------------------------------------------------------- /sample-app/front-end/src/lib/SearchClient.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const javaRestApiServer = (process.env.VUE_APP_SEARCH_API_JAVA || "http://localhost:8085"); 4 | const nodeRestApiServer = (process.env.VUE_APP_SEARCH_API_NODE || "http://localhost:8086"); 5 | const pythonRestApiServer = (process.env.VUE_APP_SEARCH_API_PYTHON || "http://localhost:8087"); 6 | 7 | 8 | export const SearchClient = { 9 | 10 | async status() { 11 | return "OK"; 12 | }, 13 | 14 | async search(queryString, page, perPage, sortBy, server) { 15 | 16 | let restServer = nodeRestApiServer; 17 | 18 | if (server) { 19 | if (server.toLowerCase() == "java") { 20 | restServer = javaRestApiServer 21 | } else if (server.toLowerCase() == "node") { 22 | restServer = nodeRestApiServer 23 | } if (server.toLowerCase() == "python") { 24 | restServer = pythonRestApiServer 25 | } 26 | } 27 | 28 | let offset = perPage * page; 29 | let limit = perPage; 30 | 31 | let url = `${restServer}/api/1.0/movies/search?q=${encodeURIComponent(queryString)}&offset=${offset}&limit=${limit}`; 32 | 33 | // add sort by if present 34 | if (sortBy) { 35 | let sortOptions = sortBy.split(":"); 36 | url=url+`&sortby=${sortOptions[0]}&ascending=${sortOptions[1] === "asc"}` 37 | 38 | } 39 | 40 | console.log(`Calling (${server}) : ${url}`); 41 | 42 | return axios.get(url); 43 | 44 | }, 45 | 46 | async getMovieGroupBy(server, field) { 47 | let restServer = nodeRestApiServer; 48 | 49 | if (server) { 50 | if (server.toLowerCase() == "java") { 51 | restServer = javaRestApiServer 52 | } else if (server.toLowerCase() == "node") { 53 | restServer = nodeRestApiServer 54 | } if (server.toLowerCase() == "python") { 55 | restServer = pythonRestApiServer 56 | } 57 | } 58 | 59 | let url = `${restServer}/api/1.0/movies/group_by/${field}`; 60 | console.log(`Calling (${server}) : ${url}`); 61 | return axios.get(url); 62 | }, 63 | 64 | async getMovie(server, id) { 65 | let restServer = nodeRestApiServer; 66 | 67 | if (server) { 68 | if (server.toLowerCase() == "java") { 69 | restServer = javaRestApiServer 70 | } else if (server.toLowerCase() == "node") { 71 | restServer = nodeRestApiServer 72 | } if (server.toLowerCase() == "python") { 73 | restServer = pythonRestApiServer 74 | } 75 | } 76 | 77 | let url = `${restServer}/api/1.0/movies/${id}`; 78 | console.log(`Calling (${server}) : ${url}`); 79 | return axios.get(url); 80 | }, 81 | 82 | async updateMovie(server, id, movie) { 83 | let restServer = nodeRestApiServer; 84 | if (server) { 85 | if (server.toLowerCase() == "java") { 86 | restServer = javaRestApiServer 87 | } else if (server.toLowerCase() == "node") { 88 | restServer = nodeRestApiServer 89 | } if (server.toLowerCase() == "python") { 90 | restServer = pythonRestApiServer 91 | } 92 | } 93 | let url = `${restServer}/api/1.0/movies/${id}`; 94 | return axios.post(url, movie); 95 | }, 96 | 97 | async getMovieComment(server, movieId) { 98 | let restServer = nodeRestApiServer; 99 | if (server) { 100 | if (server.toLowerCase() == "java") { 101 | restServer = javaRestApiServer 102 | } else if (server.toLowerCase() == "node") { 103 | restServer = nodeRestApiServer 104 | } if (server.toLowerCase() == "python") { 105 | restServer = pythonRestApiServer 106 | } 107 | } 108 | let url = `${restServer}/api/1.0/movies/${movieId}/comments`; 109 | console.log(`Calling ${url}`); 110 | return axios.get(url); 111 | }, 112 | 113 | async saveNewComment(server, movieId, comment) { 114 | let restServer = nodeRestApiServer; 115 | if (server) { 116 | if (server.toLowerCase() == "java") { 117 | restServer = javaRestApiServer 118 | } else if (server.toLowerCase() == "node") { 119 | restServer = nodeRestApiServer 120 | } if (server.toLowerCase() == "python") { 121 | restServer = pythonRestApiServer 122 | } 123 | } 124 | let url = `${restServer}/api/1.0/movies/${movieId}/comments`; 125 | console.log(`Calling POST ${url}`); 126 | return axios.post(url, comment); 127 | }, 128 | 129 | async deleteCommentById(server, commentId) { 130 | let restServer = nodeRestApiServer; 131 | if (server) { 132 | if (server.toLowerCase() == "java") { 133 | restServer = javaRestApiServer 134 | } else if (server.toLowerCase() == "node") { 135 | restServer = nodeRestApiServer 136 | } if (server.toLowerCase() == "python") { 137 | restServer = pythonRestApiServer 138 | } 139 | } 140 | let url = `${restServer}/api/1.0/comments/${commentId}`; 141 | console.log(`Calling DELETE ${url}`); 142 | return axios.delete(url); 143 | }, 144 | 145 | 146 | 147 | 148 | } -------------------------------------------------------------------------------- /sample-app/front-end/src/lib/search-samples.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title" : "All Movies", 4 | "cli" : "FT.SEARCH 'idx:movie' '*'", 5 | "form" : "*" 6 | }, 7 | { 8 | "title" : "Fuzzy Search 'empre', for Empire", 9 | "cli" : "FT.SEARCH 'idx:movie' '%empre%'", 10 | "form" : "%empre%" 11 | }, 12 | { 13 | "title" : "All 'Action' Movies", 14 | "cli" : "FT.SEARCH 'idx:movie' '@genre:{Action}'", 15 | "form" : "@genre:{Action}" 16 | }, 17 | { 18 | "title" : "All movies released in 2000", 19 | "cli" : "FT.SEARCH 'idx:movie' '@release_year:[2000 2000]'", 20 | "form" : "@release_year:[2000 2000]" 21 | }, 22 | { 23 | "title" : "'Drama' from 2010 to 2020", 24 | "cli" : "FT.SEARCH 'idx:movie' '@genre:{Drama} @release_year:[2010 2020]'", 25 | "form" : "@genre:{Drama} @release_year:[2010 2020]" 26 | }, 27 | { 28 | "title" : "Star Wars Movies", 29 | "cli" : "FT.SEARCH 'idx:movie' 'star wars", 30 | "form" : "star wars" 31 | }, 32 | { 33 | "title" : "Star Wars movies that does NOT mention Jedi", 34 | "cli" : "FT.SEARCH 'idx:movie' 'star wars", 35 | "form" : "star wars -jedi" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /sample-app/front-end/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | import { BootstrapVue } from 'bootstrap-vue'; 6 | import 'bootstrap/dist/css/bootstrap.css'; 7 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 8 | 9 | import sampleQueries from "./lib/search-samples.json"; 10 | 11 | Vue.use(BootstrapVue); 12 | 13 | 14 | Vue.config.productionTip = false 15 | 16 | Vue.prototype.$sampleQueries = sampleQueries; 17 | 18 | new Vue({ 19 | router, 20 | render: h => h(App) 21 | }).$mount('#app') 22 | -------------------------------------------------------------------------------- /sample-app/front-end/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Search from '../views/Search.vue' 4 | import FacetedSearch from '../views/FacetedSearch.vue' 5 | import Home from '../views/Home.vue' 6 | import MovieForm from '../views/MovieForm.vue' 7 | 8 | Vue.use(VueRouter) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'Home', 14 | component: Home 15 | }, 16 | { 17 | path: '/search', 18 | name: 'Search', 19 | component: Search 20 | }, 21 | { 22 | path: '/faceted-search', 23 | name: 'FacetedSearch', 24 | component: FacetedSearch 25 | }, 26 | { 27 | path: '/movies/:id', 28 | name: 'MovieForm', 29 | component: MovieForm 30 | } 31 | ] 32 | 33 | const router = new VueRouter({ 34 | routes 35 | }) 36 | 37 | export default router 38 | -------------------------------------------------------------------------------- /sample-app/front-end/src/views/FacetedSearch.vue: -------------------------------------------------------------------------------- 1 | 188 | 189 | 305 | -------------------------------------------------------------------------------- /sample-app/front-end/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | -------------------------------------------------------------------------------- /sample-app/front-end/src/views/MovieForm.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 159 | 160 | -------------------------------------------------------------------------------- /sample-app/front-end/src/views/Search.vue: -------------------------------------------------------------------------------- 1 | 143 | 144 | 228 | -------------------------------------------------------------------------------- /sample-app/front-end/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | port: 8084 4 | } 5 | } -------------------------------------------------------------------------------- /sample-app/redisearch-docker/dataset/import-data.sh: -------------------------------------------------------------------------------- 1 | sleep 15 2 | redis-cli -h redis-stack -p 6379 < /dataset/import_actors.redis 3 | redis-cli -h redis-stack -p 6379 < /dataset/import_movies.redis 4 | redis-cli -h redis-stack -p 6379 < /dataset/import_users.redis 5 | redis-cli -h redis-stack -p 6379 < /dataset/import_create_index.redis 6 | -------------------------------------------------------------------------------- /sample-app/redisearch-docker/dataset/import_create_index.redis: -------------------------------------------------------------------------------- 1 | 2 | FT.CREATE idx:movie ON hash PREFIX 1 "movie:" SCHEMA title TEXT SORTABLE plot TEXT release_year NUMERIC SORTABLE rating NUMERIC SORTABLE genre TAG SORTABLE 3 | 4 | FT.CREATE idx:actor ON hash PREFIX 1 "actor:" SCHEMA first_name TEXT SORTABLE last_name TEXT SORTABLE date_of_birth NUMERIC SORTABLE 5 | 6 | FT.CREATE idx:user ON hash PREFIX 1 "user:" SCHEMA gender TAG country TAG SORTABLE last_login NUMERIC SORTABLE location GEO 7 | 8 | FT.CREATE idx:comments:movies on HASH PREFIX 1 'comments:' SCHEMA movie_id TAG SORTABLE user_id TEXT SORTABLE comment TEXT WEIGHT 1.0 timestamp NUMERIC SORTABLE rating NUMERIC SORTABLE 9 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RediSearch/redisearch-getting-started/0b5d84954f1ad9557aece8907d1ea303d62bba90/sample-app/redisearch-jedis-rest/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine as build 2 | WORKDIR /workspace/app 3 | 4 | COPY mvnw . 5 | COPY .mvn .mvn 6 | COPY pom.xml . 7 | COPY src src 8 | 9 | RUN ./mvnw install -DskipTests 10 | RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) 11 | 12 | FROM openjdk:8-jdk-alpine 13 | VOLUME /tmp 14 | ARG DEPENDENCY=/workspace/app/target/dependency 15 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib 16 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF 17 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app 18 | 19 | EXPOSE 8085 20 | 21 | ENTRYPOINT ["java","-cp","app:app/lib/*","com.redislabs.search.demo.jedis.RedisearchJedisRestApplication"] 22 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/README.md: -------------------------------------------------------------------------------- 1 | # RediSearch REST Server: Java with Jedis 2 | 3 | The goal of this application is to show how to develop a RediSearch application with Java. 4 | 5 | This project is a Spring Boot application. 6 | 7 | This application uses [JRediSearch](https://github.com/RediSearch/JRediSearch) that is based on [Jedis](https://github.com/xetorthio/jedis). 8 | 9 | This application exposes various endpoint that are directly consumable in a front end. 10 | 11 | 12 | 13 | ### Running the application in Docker 14 | 15 | You can run build and run the application from docker using the following commands: 16 | 17 | **Build** 18 | 19 | ```shell script 20 | 21 | > docker build -t redis/search-backend-java . 22 | 23 | ``` 24 | 25 | This command will create a new image and build the maven project into it. 26 | 27 | **Run** 28 | 29 | ```shell script 30 | > docker run --rm \ 31 | --env "REDIS_URL=redis://redis-stack:6379" \ 32 | --env "REDIS_INDEX=idx:movie" \ 33 | --name "redisearch-backend-java"\ 34 | -p 8085:8085 redis/redis-stack:latest 35 | ``` 36 | 37 | You can now access the REST Search service using the following URL: 38 | 39 | * http://localhost:8085/api/1.0/movies/search?q=man&limit=10&offset=20 40 | 41 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.0 9 | 10 | 11 | com.redislabs.search 12 | redisearch-jedis-rest 13 | 0.0.1-SNAPSHOT 14 | redisearch-jedis-rest 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | 4.2.2 20 | 1 21 | 22 | 23 | 24 | 25 | snapshots-repo 26 | https://oss.sonatype.org/content/repositories/snapshots 27 | 28 | 29 | 30 | 31 | 32 | 33 | redis.clients 34 | jedis 35 | ${version.jedis} 36 | 37 | 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | 46 | 47 | org.projectlombok 48 | lombok 49 | 50 | 51 | 52 | javax.inject 53 | javax.inject 54 | ${version.javax.inject} 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-test 61 | test 62 | 63 | 64 | org.junit.vintage 65 | junit-vintage-engine 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-maven-plugin 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 3.10.1 82 | 83 | 1.8 84 | 1.8 85 | 86 | 87 | 88 | org.projectlombok 89 | lombok 90 | 1.18.24 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/src/main/java/com/redislabs/search/demo/jedis/RediSearchRestController.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.search.demo.jedis; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.*; 6 | 7 | import javax.inject.Inject; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | @Slf4j 12 | @CrossOrigin(origins = "*") 13 | @RequestMapping("/api/1.0/") 14 | @RestController 15 | public class RediSearchRestController { 16 | 17 | @Inject RediSearchService rediSearchService; 18 | 19 | 20 | @GetMapping("/status") 21 | public String status() { 22 | return "OK"; 23 | } 24 | 25 | 26 | @GetMapping("/movies/search") 27 | public Map search( 28 | @RequestParam(name="q")String query, 29 | @RequestParam(name="offset", defaultValue="0")int offset, 30 | @RequestParam(name="limit", defaultValue="10")int limit, 31 | @RequestParam(name="sortby", defaultValue="")String sortBy, 32 | @RequestParam(name="ascending", defaultValue="true")boolean ascending) { 33 | return rediSearchService.search(query, offset, limit, sortBy,ascending); 34 | } 35 | 36 | @GetMapping("/movies/group_by/{field}") 37 | public Map getMovieGroupBy(@PathVariable("field") String field) { 38 | return rediSearchService.getMovieGroupBy(field); 39 | } 40 | 41 | @GetMapping("/movies/search_with_command") 42 | public Map searchWithJedisCommand( 43 | @RequestParam(name="q")String query, 44 | @RequestParam(name="offset", defaultValue="0")int offset, 45 | @RequestParam(name="limit", defaultValue="10")int limit, 46 | @RequestParam(name="sortby", defaultValue="")String sortBy, 47 | @RequestParam(name="ascending", defaultValue="true")boolean ascending) { 48 | return rediSearchService.searchWithJedisCommand(query, offset, limit, sortBy, ascending); 49 | } 50 | 51 | @GetMapping("/movies/{movieId}") 52 | @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 53 | public Map getMOvieById(@PathVariable("movieId") String movieId) { 54 | Map result = new HashMap<>(); 55 | result.put("messsage", "This movie endpoint is not implemented in Java, use the Node.js Endpoint"); 56 | return result; 57 | }; 58 | 59 | @PostMapping("/movies/{movieId}") 60 | @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 61 | public Map saveMovie(@PathVariable("movieId") String movieId) { 62 | Map result = new HashMap<>(); 63 | result.put("messsage", "This movie endpoint is not implemented in Java, use the Node.js Endpoint"); 64 | return result; 65 | }; 66 | 67 | @GetMapping("/movies/{movieId}/comments") 68 | @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 69 | public Map getMovieComments(@PathVariable("movieId") String movieId) { 70 | Map result = new HashMap<>(); 71 | result.put("messsage", "Comment API not implemented in Java, use the Node.js Endpoint"); 72 | return result; 73 | }; 74 | 75 | @PostMapping("/movies/{movieId}/comments") 76 | @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 77 | public Map createMovieComments(@PathVariable("movieId") String movieId) { 78 | Map result = new HashMap<>(); 79 | result.put("messsage", "Comment API not implemented in Java, use the Node.js Endpoint"); 80 | return result; 81 | }; 82 | 83 | @GetMapping("/comments/{commentId}") 84 | @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 85 | public Map getCommentById(@PathVariable("commentId") String commentId) { 86 | Map result = new HashMap<>(); 87 | result.put("messsage", "Comment API not implemented in Java, use the Node.js Endpoint"); 88 | return result; 89 | }; 90 | 91 | @DeleteMapping("/comments/{commentId}") 92 | @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 93 | public Map deleteCommentById(@PathVariable("commentId") String commentId) { 94 | Map result = new HashMap<>(); 95 | result.put("messsage", "Comment API not implemented in Java, use the Node.js Endpoint"); 96 | return result; 97 | }; 98 | 99 | } 100 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/src/main/java/com/redislabs/search/demo/jedis/RediSearchService.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.search.demo.jedis; 2 | 3 | import redis.clients.jedis.*; 4 | 5 | import redis.clients.jedis.search.*; 6 | import redis.clients.jedis.search.SearchResult; 7 | import redis.clients.jedis.search.aggr.AggregationBuilder; 8 | import redis.clients.jedis.search.aggr.AggregationResult; 9 | import redis.clients.jedis.search.aggr.Reducers; 10 | import redis.clients.jedis.search.aggr.SortedField; 11 | 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.core.env.Environment; 15 | import org.springframework.stereotype.Service; 16 | 17 | import javax.annotation.PostConstruct; 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | import java.util.*; 21 | 22 | import static redis.clients.jedis.search.RediSearchCommands.*; 23 | 24 | @Slf4j 25 | @Service 26 | public class RediSearchService { 27 | 28 | @Autowired 29 | private Environment env; 30 | 31 | JedisPooled client; 32 | // Client rediSearchClient; 33 | 34 | String indexName = "idx:movie"; // default name 35 | String redisUrl = "redis://localhost:6379"; // default name 36 | 37 | @PostConstruct 38 | private void init() throws URISyntaxException { 39 | log.info("Init RediSearchService"); 40 | 41 | // Get the configuration from the application properties/environment 42 | indexName = env.getProperty("redis.index"); 43 | redisUrl = env.getProperty("redis.url"); 44 | 45 | log.info("Configuration Index: "+indexName+" - redisUrl: "+redisUrl); 46 | 47 | client = new JedisPooled(new URI(redisUrl)); 48 | 49 | } 50 | 51 | /** Execute the search query with 52 | * some parameter 53 | * @param queryString 54 | * @param offset 55 | * @param limit 56 | * @param sortBy 57 | * @return an object with meta: query header and docs: the list of documents 58 | */ 59 | public Map search(String queryString, int offset, int limit, String sortBy, boolean ascending ){ 60 | // Let's put all the informations in a Map top make it easier to return JSON object 61 | // no need to have "predefine mapping" 62 | Map returnValue = new HashMap<>(); 63 | Map resultMeta = new HashMap<>(); 64 | 65 | // Create a simple query 66 | Query query = new Query(queryString) 67 | .setWithScores() 68 | .limit(offset, limit); 69 | // if sort by parameter add it to the query 70 | if (sortBy != null && !sortBy.isEmpty()) { 71 | query.setSortBy(sortBy, ascending); // Ascending by default 72 | } 73 | 74 | // Execute the query 75 | SearchResult queryResult = client.ftSearch(indexName, query); 76 | 77 | // Adding the query string for information purpose 78 | resultMeta.put("queryString",queryString); 79 | 80 | // Get the total number of documents and other information for this query: 81 | resultMeta.put("totalResults", queryResult.getTotalResults()); 82 | resultMeta.put("offset", offset); 83 | resultMeta.put("limit", limit); 84 | 85 | returnValue.put("meta", resultMeta); 86 | 87 | // the docs are returned as an array of document, with the document itself being a list of k/v json documents 88 | // not the easiest to manipulate 89 | // the `raw_docs` is used to view the structure 90 | // the `docs` will contain the list of document that is more developer friendly 91 | // capture in https://github.com/RediSearch/JRediSearch/issues/121 92 | // returnValue.put("raw_docs", queryResult.docs); 93 | returnValue.put("raw_docs", queryResult.getDocuments()); 94 | 95 | 96 | // remove the properties array and create attributes 97 | List> docsToReturn = new ArrayList<>(); 98 | List docs = queryResult.getDocuments(); 99 | 100 | for (Document doc :docs) { 101 | 102 | Map props = new HashMap<>(); 103 | Map meta = new HashMap<>(); 104 | meta.put("id", doc.getId()); 105 | meta.put("score", doc.getScore()); 106 | doc.getProperties().forEach( e -> { 107 | props.put( e.getKey(), e.getValue() ); 108 | }); 109 | 110 | Map docMeta = new HashMap<>(); 111 | docMeta.put("meta",meta); 112 | docMeta.put("fields",props); 113 | docsToReturn.add(docMeta); 114 | } 115 | 116 | returnValue.put("docs", docsToReturn); 117 | 118 | return returnValue; 119 | } 120 | 121 | public Map search(String queryString ){ 122 | return search(queryString, 0, 10, null, true); 123 | } 124 | 125 | public Map getMovieGroupBy(String groupByField) { 126 | Map result = new HashMap<>(); 127 | 128 | // Create an aggregation query that list the genre 129 | // FT.AGGREGATE idx:movie "*" GROUPBY 1 @genre REDUCE COUNT 0 AS nb_of_movies SORTBY 2 @genre ASC 130 | AggregationBuilder aggregation = new AggregationBuilder() 131 | .groupBy("@"+groupByField, Reducers.count().as("nb_of_movies")) 132 | .sortBy( SortedField.asc("@"+groupByField)) 133 | .limit(0,1000); // get all rows 134 | 135 | AggregationResult aggrResult = client.ftAggregate(indexName, aggregation); 136 | int resultSize = aggrResult.getResults().size(); 137 | 138 | List> docsToReturn = new ArrayList<>(); 139 | List> results = aggrResult.getResults(); 140 | 141 | result.put("totalResults",aggrResult.totalResults); 142 | 143 | List> formattedResult = new ArrayList<>(); 144 | 145 | // get all result rows and format them 146 | for (int i = 0; i < resultSize ; i++) { 147 | Map entry = new HashMap<>(); 148 | entry.put(groupByField, aggrResult.getRow(i).getString(groupByField)); 149 | entry.put("nb_of_movies", aggrResult.getRow(i).getLong("nb_of_movies")); 150 | formattedResult.add(entry); 151 | } 152 | result.put("rows", formattedResult); 153 | return result; 154 | } 155 | 156 | public Map searchWithJedisCommand(String queryString, int offset, int limit, String sortBy, boolean ascending) { 157 | Map returnValue = new HashMap<>(); 158 | Map resultMeta = new HashMap<>(); 159 | 160 | // Create list of parameter for the FT.SEARCH command 161 | List commandParams = new ArrayList<>(); // TODO move to List.of when moving to new JDK 162 | commandParams.add(indexName); 163 | commandParams.add(queryString); 164 | commandParams.add("WITHSCORES"); // return the score in the document 165 | 166 | //set the limit 167 | commandParams.add("LIMIT"); 168 | commandParams.add(String.valueOf(offset)); 169 | commandParams.add(String.valueOf(limit)); 170 | 171 | // if sortby add the paramter 172 | if (sortBy != null && !sortBy.isEmpty()){ 173 | commandParams.add("SORTBY"); 174 | commandParams.add(sortBy); 175 | commandParams.add((ascending)?"ASC":"DESC"); 176 | 177 | } 178 | 179 | log.info(commandParams.toString()); 180 | 181 | List result = (ArrayList)client.sendCommand( 182 | com.redislabs.search.demo.jedis.util.RediSearchCommands.Command.SEARCH, 183 | commandParams.toArray(new String[0])); 184 | 185 | // The result of the command: 186 | // * depends of the parameters your send (in this case it will be very simple) 187 | // * is a list of list, ... that matches the structure of the CLI 188 | // FT.SEARCH idx:movie "@title:(heat -woman)" => The command (not part of the result) 189 | 190 | // List entry #0 => number of documents 191 | // 1) (integer) 2 192 | 193 | // List entry #1 => if not parameters, the first id of the document 194 | // 2) "movie:1141" => if not other option, the list of document start, and the 2nd entry is the ID of the doc 195 | 196 | // List entry #2 => another list, that contains a list of string, with field name, field value 197 | // 3) 1) "poster" 198 | // 2) "https://m...L_.jpg" 199 | // 3) "genre" 200 | // 4) "Drama" 201 | // 5) "title" 202 | // 6) "Heat" 203 | // ... 204 | // 15) "plot" 205 | // 16) "A .. heist." 206 | // List entry #3,5, ... : movie id 207 | // 4) "movie:818" 208 | 209 | // List entry #4,6,8 => nother list with all the fields and values 210 | // 5) 1) "poster" 211 | // 2) "N/A" 212 | // 3) "genre" 213 | // 4) "Comedy" 214 | // 5) "title" 215 | // 6) "California Heat" 216 | // ... 217 | // 15) "plot" 218 | // 16) "A ..." 219 | 220 | // Time to process this list. 221 | 222 | 223 | // the first element is always the number of results 224 | Long totalResults = (Long) result.get(0); 225 | List docs = new ArrayList<>(result.size() - 1); 226 | int stepForDoc = 3; // iterate over doc_id/score/values 227 | 228 | List> docList = new ArrayList<>(); 229 | 230 | 231 | if (totalResults != 0) { 232 | for (int i = 1; i < result.size(); i += stepForDoc) { 233 | 234 | Map meta = new HashMap<>(); 235 | String docId = new String((byte[]) result.get(i)); 236 | 237 | Double score = Double.valueOf(new String((byte[]) result.get(i+1))); 238 | meta.put("id", docId); 239 | meta.put("score", score); 240 | 241 | // parse the list of fields and create a map of it 242 | Map fieldsMap = new HashMap<>(); 243 | List fields = (List) result.get(i + 2); 244 | for (int j = 0; j < fields.size(); j += 2) { 245 | String fieldName = new String((byte[]) fields.get(j)); 246 | String fieldValue = new String((byte[]) fields.get(j+1)); 247 | fieldsMap.put(fieldName, fieldValue); 248 | } 249 | 250 | Mapdoc = new HashMap<>(); 251 | doc.put("meta", meta); 252 | doc.put("fields", fieldsMap); 253 | docList.add(doc); 254 | } 255 | } 256 | 257 | resultMeta.put("totalResults", totalResults); 258 | resultMeta.put("queryString", queryString); 259 | resultMeta.put("offset", offset); 260 | resultMeta.put("limit", limit); 261 | 262 | returnValue.put("meta", resultMeta); 263 | returnValue.put("docs", docList); 264 | 265 | return returnValue; 266 | } 267 | 268 | /** 269 | * This method does NOT use JRediSearch library but the Jedis command directly 270 | * 271 | * This is to show the various options, since some developers want to stay as close as possible from the Redis Commands 272 | * 273 | * @param queryString 274 | * @return 275 | */ 276 | public Map searchWithJedisCommand(String queryString) { 277 | return searchWithJedisCommand(queryString, 0, 10, null, true); 278 | } 279 | 280 | 281 | } 282 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/src/main/java/com/redislabs/search/demo/jedis/RedisearchJedisRestApplication.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.search.demo.jedis; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RedisearchJedisRestApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RedisearchJedisRestApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/src/main/java/com/redislabs/search/demo/jedis/util/RediSearchCommands.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.search.demo.jedis.util; 2 | 3 | import redis.clients.jedis.commands.ProtocolCommand; 4 | import redis.clients.jedis.util.SafeEncoder; 5 | 6 | /** 7 | * This class is obviously not necessary when you are using JRediSearch 8 | * 9 | * This is just to demonstrate how to use RediSearch using Jedis only 10 | * 11 | * The first thing to do is to add new commands. 12 | */ 13 | public class RediSearchCommands { 14 | 15 | public enum Command implements ProtocolCommand { 16 | SEARCH("FT.SEARCH"); 17 | 18 | private final byte[] raw; 19 | 20 | Command(String alt) { 21 | raw = SafeEncoder.encode(alt); 22 | } 23 | 24 | @Override 25 | public byte[] getRaw() { 26 | return raw; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sample-app/redisearch-jedis-rest/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | redis.url=redis://localhost:6379 3 | redis.index=idx:movie 4 | 5 | server.port=8085 6 | -------------------------------------------------------------------------------- /sample-app/redisearch-node-rest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | COPY . . 9 | 10 | EXPOSE 8086 11 | 12 | CMD [ "node", "server.js" ] 13 | -------------------------------------------------------------------------------- /sample-app/redisearch-node-rest/NodeSearchService.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | const redisearch = require('redis-redisearch'); 3 | 4 | const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; 5 | const indexNameMovies = process.env.REDIS_INDEX || 'idx:movie'; 6 | const indexNameComments = process.env.REDIS_INDEX_COMMENTS || 'idx:comments:movies'; 7 | 8 | console.log(`Configuration Index: ${indexNameMovies} - redisUrl: ${redisUrl}`); 9 | 10 | redisearch(redis); 11 | const client = redis.createClient(redisUrl); 12 | 13 | 14 | const SearchService = function () { 15 | 16 | const _search = function (queryString, options, callback) { 17 | 18 | let offset = 0; // default values 19 | let limit = 10; // default value 20 | 21 | 22 | // prepare the "native" FT.SEARCH call 23 | // FT.SEARCH IDX_NAME queryString [options] 24 | const searchParams = [ 25 | indexNameMovies, // name of the index 26 | queryString, // query string 27 | 'WITHSCORES' // return the score 28 | ]; 29 | 30 | // if limit add the parameters 31 | if (options.offset || options.limit) { 32 | offset = options.offset || 0; 33 | limit = options.limit || 10 34 | searchParams.push('LIMIT'); 35 | searchParams.push(offset); 36 | searchParams.push(limit); 37 | } 38 | // if sortby add the parameters 39 | if (options.sortBy) { 40 | searchParams.push('SORTBY'); 41 | searchParams.push(options.sortBy); 42 | searchParams.push((options.ascending) ? 'ASC' : 'DESC'); 43 | } 44 | 45 | console.log(searchParams); 46 | 47 | client.ft_search( 48 | searchParams, 49 | function (err, searchResult) { 50 | 51 | const totalNumberOfDocs = searchResult[0]; 52 | const result = { 53 | meta: { 54 | totalResults: totalNumberOfDocs, 55 | offset, 56 | limit, 57 | queryString, 58 | }, 59 | docs: [], 60 | raw_docs: searchResult 61 | } 62 | 63 | // create JSON document from n/v pairs 64 | for (let i = 1; i <= searchResult.length - 1; i++) { 65 | const doc = { 66 | meta: { 67 | score: Number(searchResult[i + 1]), 68 | id: searchResult[i] 69 | } 70 | }; 71 | i = i + 2; 72 | doc.fields = {}; 73 | const fields = searchResult[i] 74 | if (fields) { 75 | for (let j = 0, len = fields.length; j < len; j++) { 76 | const idxKey = j; 77 | const idxValue = idxKey + 1; 78 | j++; 79 | doc.fields[fields[idxKey]] = fields[idxValue]; 80 | } 81 | } 82 | result.docs.push(doc); 83 | } 84 | 85 | callback(err, result); 86 | } 87 | ); 88 | 89 | } 90 | 91 | const _getMovieGroupBy = function (field, callback) { 92 | const retValue = { 93 | totalResults: 0, 94 | rows: [], 95 | raw: [] // get the data as returned by the API 96 | }; 97 | 98 | // prepare the "native" FT.AGGREGATE call 99 | // FT.AGGREGATE IDX_NAME queryString [options] 100 | const pipeline = [ 101 | indexNameMovies, // name of the index 102 | '*', // query string, 103 | 'GROUPBY', '1', `@${field}`, // group by 104 | 'REDUCE', 'COUNT', '0', 'AS', 'nb_of_movies', //count the number of movies by group 105 | 'SORTBY', '2', `@${field}`, 'ASC', // sorted by the genre 106 | 'LIMIT', '0', '1000' // get all genre expecting less than 100 genres 107 | ]; 108 | 109 | client.ft_aggregate( 110 | pipeline, 111 | function (err, aggrResult) { 112 | 113 | // transform array into document 114 | // this should be added to a generic function 115 | // ideally into the library itself 116 | retValue.totalResults = aggrResult[0]; 117 | 118 | // loop on the results starting at element 1 119 | for (let i = 1; i <= aggrResult.length - 1; i++) { 120 | const item = aggrResult[i]; 121 | const doc = {}; 122 | for (let j = 0, len = item.length; j < len; j++) { 123 | doc[item[j]] = item[j + 1]; 124 | doc[item[j + 2]] = item[j + 3]; 125 | j = j + 3; 126 | } 127 | retValue.rows.push(doc); 128 | } 129 | retValue.raw = aggrResult; 130 | callback(err, retValue); 131 | }); 132 | 133 | } 134 | 135 | const _getMovie = function (id, callback) { 136 | // if id does not start with `movie:` add it 137 | if (!id.startsWith('movie:')) { 138 | id = 'movie:' + id; 139 | } 140 | client.hgetall(id, function (err, movie) { 141 | if (!movie) { 142 | movie = { 143 | ibmdb_id: null, 144 | genre: null, 145 | poster: null, 146 | rating: null, 147 | votes: null, 148 | title: null, 149 | plot: null, 150 | release_year: null 151 | }; 152 | } 153 | callback(err, movie); 154 | }); 155 | } 156 | 157 | /** 158 | * Update the movie with that as the key `id` 159 | * @param {*} id 160 | * @param {*} movie 161 | * @param {*} callbacl 162 | */ 163 | const _saveMovie = function (id, movie, callback) { 164 | // if id does not start with `movie:` add it 165 | if (!id.startsWith('movie:')) { 166 | id = 'movie:' + id; 167 | } 168 | client.hmset(id, movie, function (err, result) { 169 | callback(err, result); 170 | }); 171 | 172 | } 173 | 174 | /** 175 | * 176 | * @param {*} id 177 | * @param {*} callback 178 | */ 179 | const _getComments = function (movieId, options, callback) { 180 | // Store only the movie id number 181 | if (movieId.startsWith('movie:')) { 182 | movieId = movieId.split(":")[1]; 183 | } 184 | let queryString = `@movie_id:{${movieId}}` 185 | let offset = 0; // default values 186 | let limit = 10; // default value 187 | 188 | 189 | // prepare the "native" FT.SEARCH call 190 | // FT.SEARCH IDX_NAME queryString [options] 191 | const searchParams = [ 192 | indexNameComments, // name of the index 193 | queryString, // query string 194 | 'WITHSCORES' // return the score 195 | ]; 196 | 197 | // if limit add the parameters 198 | if (options.offset || options.limit) { 199 | offset = options.offset || 0; 200 | limit = options.limit || 10 201 | searchParams.push('LIMIT'); 202 | searchParams.push(offset); 203 | searchParams.push(limit); 204 | } 205 | // if sortby add the parameters 206 | if (options.sortBy) { 207 | searchParams.push('SORTBY'); 208 | searchParams.push(options.sortBy); 209 | searchParams.push((options.ascending) ? 'ASC' : 'DESC'); 210 | } else { 211 | searchParams.push('SORTBY'); 212 | searchParams.push('timestamp'); 213 | searchParams.push('DESC'); 214 | } 215 | 216 | console.log(searchParams) 217 | 218 | 219 | client.ft_search( 220 | searchParams, 221 | function (err, searchResult) { 222 | 223 | 224 | console.log(searchResult) 225 | 226 | const totalNumberOfDocs = searchResult[0]; 227 | const result = { 228 | meta: { 229 | totalResults: totalNumberOfDocs, 230 | offset, 231 | limit, 232 | queryString, 233 | }, 234 | docs: [], 235 | } 236 | 237 | // create JSON document from n/v pairs 238 | for (let i = 1; i <= searchResult.length - 1; i++) { 239 | const doc = { 240 | meta: { 241 | score: Number(searchResult[i + 1]), 242 | id: searchResult[i] 243 | } 244 | }; 245 | i = i + 2; 246 | doc.fields = {}; 247 | const fields = searchResult[i] 248 | if (fields) { 249 | for (let j = 0, len = fields.length; j < len; j++) { 250 | const idxKey = j; 251 | const idxValue = idxKey + 1; 252 | j++; 253 | doc.fields[fields[idxKey]] = fields[idxValue]; 254 | 255 | // To make it easier let's format the timestamp 256 | if (fields[idxKey] == "timestamp") { 257 | const date = new Date(parseInt(fields[idxValue])); 258 | doc.fields["dateAsString"] = date.toDateString() + " - " + date.toLocaleTimeString(); 259 | } 260 | } 261 | } 262 | result.docs.push(doc); 263 | } 264 | 265 | callback(err, result); 266 | } 267 | ); 268 | 269 | 270 | 271 | } 272 | 273 | /** 274 | * 275 | * @param {*} movieId 276 | * @param {*} comment 277 | * @param {*} callback 278 | */ 279 | const _saveComment = function (movieId, comment, callback) { 280 | 281 | // Store only the movie id number 282 | if (movieId.startsWith('movie:')) { 283 | movieId = movieId.split(":")[1]; 284 | } 285 | 286 | // Add the movie id to the comment 287 | comment.movie_id = movieId; 288 | 289 | const ts = Date.now(); 290 | const key = `comments:movie:${comment.movie_id}:${ts}` 291 | comment.timestamp = ts; 292 | 293 | const values = [ 294 | "movie_id", comment.movie_id, 295 | "user_id", comment.user_id, 296 | "comment", comment.comment, 297 | "rating", comment.rating, 298 | "timestamp", comment.timestamp, 299 | ]; 300 | client.hmset(key, values, function (err, res) { 301 | callback(err, { "id": key, "comment": comment }); 302 | }); 303 | 304 | } 305 | 306 | /** 307 | * Delete a comment 308 | * @param {*} commentId 309 | * @param {*} callback 310 | */ 311 | const _deleteComment = function (commentId, callback) { 312 | client.del(commentId, function (err, res) { 313 | callback(err, res); 314 | }); 315 | } 316 | 317 | const _getCommentById = function (commentId, callback) { 318 | // using hgetall, since the hash size is limited 319 | client.hgetall(commentId, function (err, res) { 320 | callback(err, res); 321 | }); 322 | } 323 | 324 | return { 325 | getMovie: _getMovie, 326 | saveMovie: _saveMovie, 327 | search: _search, 328 | getMovieGroupBy: _getMovieGroupBy, 329 | getCommentById: _getCommentById, 330 | getComments: _getComments, 331 | saveComment: _saveComment, 332 | deleteComment: _deleteComment, 333 | }; 334 | } 335 | 336 | module.exports = SearchService; 337 | -------------------------------------------------------------------------------- /sample-app/redisearch-node-rest/README.md: -------------------------------------------------------------------------------- 1 | # RediSearch: Node.js Sample 2 | 3 | 4 | 5 | 6 | ## Coding the application 7 | 8 | #### 1- Create the Project 9 | 10 | Follow the `npm` steps 11 | 12 | ``` 13 | $ npm init 14 | ``` 15 | 16 | #### 2- Add Dependencies 17 | 18 | Add the dependencies: 19 | 20 | * [Express](https://www.npmjs.com/package/express) 21 | * [Node RediSearch](https://www.npmjs.com/package/redis-redisearch) 22 | 23 | ``` 24 | $ npm install express redis redis-redisearch --save 25 | ``` 26 | 27 | 28 | #### 3- Create REST API Routes 29 | 30 | Create the `server.js` file and add the following code 31 | 32 | ```js 33 | const express = require('express') 34 | const app = express() 35 | const port = 3003 36 | 37 | 38 | app.get('/api/1.0/', (req, res) => { 39 | res.json({"status" : "started"}); 40 | }) 41 | 42 | 43 | app.get('/', (req, res) => { 44 | res.send('RediSearch Node REST Server Started') 45 | }) 46 | 47 | app.listen(port, () => { 48 | console.log(`RediSearch Node listening at http://localhost:${port}`) 49 | }) 50 | 51 | ``` 52 | 53 | This will be the base of the various API endpoints. 54 | 55 | 56 | #### 4- Create a NodeSearchService 57 | 58 | In this sample application all the RediSearch interactions will be done in `NodeSearchService.js` file. 59 | 60 | 61 | ### Running the application in Docker 62 | 63 | You can run build and run the application from docker using the following commands: 64 | 65 | **Build** 66 | 67 | ```shell script 68 | 69 | > docker build -t redis/search-backend-node . 70 | 71 | ``` 72 | 73 | This command will create a new image and build the Node.js project into it. 74 | 75 | **Run** 76 | 77 | ```shell script 78 | > docker run --rm \ 79 | --env "REDIS_URL=redis://host.docker.internal:6379" \ 80 | --env "REDIS_INDEX=idx:movie" \ 81 | --name "redisearch-backend-node"\ 82 | -p 8086:8086 redis/redis-stack:latest 83 | ``` 84 | 85 | ### Running the application locally 86 | 87 | To run the application on your local machine: 88 | 89 | ```shell script 90 | > npm install 91 | > npm start 92 | ``` 93 | 94 | ### Accessing the API 95 | 96 | You can now access the REST Search service using the following URL: 97 | 98 | * http://localhost:8086/api/1.0/movies/search?q=man&limit=10&offset=20 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /sample-app/redisearch-node-rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redisearch-node-rest", 3 | "version": "1.0.0", 4 | "description": "Sample RediSearch REST Server with Node.js", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "engines": { 11 | "node": ">=8.9.4" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Redis-Developer/getting-started-redisearch.git" 16 | }, 17 | "keywords": [ 18 | "redis", 19 | "redisearch", 20 | "node" 21 | ], 22 | "author": "Tugdual Grall", 23 | "license": "Apache-2.0", 24 | "bugs": { 25 | "url": "https://github.com/Redis-Developer/getting-started-redisearch/issues" 26 | }, 27 | "homepage": "https://github.com/Redis-Developer/getting-started-redisearch#readme", 28 | "dependencies": { 29 | "cors": "^2.8.5", 30 | "express": "^4.17.1", 31 | "redis": "^3.0.2", 32 | "redis-redisearch": "stockholmux/node_redis-redisearch" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample-app/redisearch-node-rest/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const app = express(); 4 | const bodyParser = require('body-parser'); 5 | const serverPort = process.env.SERVER_PORT || 8086; 6 | 7 | 8 | 9 | const SearchService = require('./NodeSearchService'); 10 | const searchService = new SearchService(); 11 | 12 | app.use(bodyParser.urlencoded({ extended: true })); 13 | app.use(cors()); 14 | app.use(bodyParser.json()); 15 | 16 | app.get('/api/1.0/movies/search', (req, res) => { 17 | const queryString = req.query.q; 18 | const offset = Number((req.query.offset)?req.query.offset:'0'); 19 | const limit = Number((req.query.limit)?req.query.limit:'10'); 20 | const sortBy = req.query.sortby; 21 | const ascending = req.query.ascending; 22 | 23 | const options = { 24 | offset, 25 | limit 26 | }; 27 | 28 | if (sortBy) { 29 | options.sortBy = sortBy; 30 | options.ascending = true; // if sorted by default it is ascending 31 | } 32 | 33 | if (ascending) { 34 | options.ascending = (ascending==1 || ascending.toLocaleLowerCase()==='true'); 35 | } 36 | 37 | searchService.search( 38 | queryString, // query string 39 | options, // options 40 | function(err, result){ // callback 41 | res.json(result); 42 | } 43 | ); 44 | }) 45 | 46 | app.get('/api/1.0/movies/group_by/:field', (req, res) =>{ 47 | searchService.getMovieGroupBy(req.params.field, function(err, result){ 48 | res.json(result); 49 | }); 50 | }); 51 | 52 | app.get('/api/1.0/movies/:id', (req, res) =>{ 53 | searchService.getMovie(req.params.id, function(err, result){ 54 | res.json(result); 55 | }); 56 | }); 57 | 58 | app.post('/api/1.0/movies/:id', (req, res) =>{ 59 | searchService.saveMovie(req.params.id, req.body, function(err, result){ 60 | res.json(result); 61 | }); 62 | }); 63 | 64 | app.get('/api/1.0/movies/:id/comments', (req, res) =>{ 65 | searchService.getComments(req.params.id, {}, function(err, result){ 66 | res.json(result); 67 | }); 68 | }); 69 | 70 | app.post('/api/1.0/movies/:id/comments', (req, res) =>{ 71 | searchService.saveComment(req.params.id, req.body, function(err, result){ 72 | res.json(result); 73 | }); 74 | }); 75 | 76 | app.get('/api/1.0/comments/:id', (req, res) =>{ 77 | searchService.getCommentById(req.params.id, function(err, result){ 78 | res.json(result); 79 | }); 80 | }); 81 | 82 | app.delete('/api/1.0/comments/:id', (req, res) =>{ 83 | searchService.deleteComment(req.params.id, function(err, result){ 84 | res.json(result); 85 | }); 86 | }); 87 | 88 | app.get('/api/1.0/', (req, res) => { 89 | res.json({status: 'started'}); 90 | }); 91 | 92 | app.get('/', (req, res) => { 93 | res.send('RediSearch Node REST Server Started'); 94 | }); 95 | 96 | app.listen(serverPort, () => { 97 | console.log(`RediSearch Node listening at http://localhost:${serverPort}`); 98 | }); 99 | -------------------------------------------------------------------------------- /sample-app/redisearch-python-rest/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=server.py 2 | FLASK_ENV=development -------------------------------------------------------------------------------- /sample-app/redisearch-python-rest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /app 4 | ADD . /app 5 | 6 | RUN pip install -r requirements.txt 7 | 8 | EXPOSE 8087 9 | 10 | 11 | CMD ["python", "server.py"] 12 | -------------------------------------------------------------------------------- /sample-app/redisearch-python-rest/README.md: -------------------------------------------------------------------------------- 1 | # RediSearch REST Server: Python 2 | 3 | 4 | 5 | ### Running the application in Docker 6 | 7 | You can run build and run the application from docker using the following commands: 8 | 9 | **Build** 10 | 11 | ```shell script 12 | 13 | > docker build -t redis/search-backend-python . 14 | 15 | ``` 16 | 17 | This command will create a new image and build the maven project into it. 18 | 19 | **Run** 20 | 21 | ```shell script 22 | > docker run --rm \ 23 | --env "REDIS_URL=redis://redis-stack:6379" \ 24 | --env "REDIS_INDEX=idx:movie" \ 25 | --name "redisearch-backend-python"\ 26 | -p 8087:8087 redis/redis-stack:latest 27 | ``` 28 | 29 | You can now access the REST Search service using the following URL: 30 | 31 | * http://localhost:8087/api/1.0/movies/search?q=man&limit=10&offset=20 32 | 33 | -------------------------------------------------------------------------------- /sample-app/redisearch-python-rest/requirements.txt: -------------------------------------------------------------------------------- 1 | redis>=4.3.3 2 | Flask>=2.1.2 3 | python-dotenv==0.20.0 4 | Flask-Cors>=3.0.10 5 | -------------------------------------------------------------------------------- /sample-app/redisearch-python-rest/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, request, json, jsonify 4 | from flask_cors import CORS 5 | 6 | import redis 7 | from os import environ 8 | from redis.commands.search import reducers 9 | from redis.commands.search.query import NumericFilter, Query 10 | import redis.commands.search.aggregation as aggregations 11 | 12 | redis_server = environ.get('REDIS_SERVER', "localhost") 13 | sys.stdout.write("redis server set to " + redis_server) 14 | 15 | redis_port = int(environ.get('REDIS_PORT', "6379")) 16 | sys.stdout.write("redis port is " + str(redis_port)) 17 | 18 | server_port = int(environ.get('SERVER_PORT', "8087")) 19 | sys.stdout.write("server port is " + str(server_port)) 20 | 21 | redis_index = environ.get('REDIS_INDEX', "idx:movie") 22 | sys.stdout.write("redis index is " + redis_index) 23 | 24 | redis_password = environ.get('REDIS_PASSWORD', "") 25 | 26 | # conn = redis.StrictRedis(redis_server, redis_port) 27 | if redis_password is not None: 28 | conn = redis.Redis(redis_server, redis_port, password=redis_password, charset="utf-8", decode_responses=True) 29 | else: 30 | conn = redis.Redis(redis_server, redis_port, charset="utf-8", decode_responses=True) 31 | 32 | app = Flask(__name__) 33 | CORS(app) 34 | 35 | 36 | # @app.before_request 37 | # @app.route('/') 38 | # def home(): 39 | # return {"status": "Python REST Servicve is UP", "api": "/api/1.0/search"} 40 | 41 | 42 | @app.route('/api/1.0/movies/search') 43 | def search(): 44 | offset = 0; 45 | limit = 10; 46 | queryString = ""; 47 | 48 | if (request.args.get('offset')): 49 | offset = int(request.args.get('offset')); 50 | 51 | if (request.args.get('limit')): 52 | limit = int(request.args.get('limit')); 53 | 54 | if (request.args.get('q')): 55 | queryString = request.args.get('q'); 56 | q = Query(queryString).with_scores().paging(offset, limit); 57 | if (request.args.get('sortby')): 58 | ascending = True; 59 | if (request.args.get('ascending')): 60 | ascending = (request.args.get('ascending').lower() == "true" or request.args.get('ascending') == "1"); 61 | 62 | q.sort_by(request.args.get('sortby'), asc=ascending); 63 | 64 | searchResult = conn.ft(index_name=redis_index).search(q); 65 | 66 | dictResult = { 67 | "meta": { 68 | "totalResults": getattr(searchResult, "total"), 69 | "offset": offset, 70 | "limit": limit, 71 | "queryString": queryString}, 72 | "docs": docs_to_dict(searchResult.docs) 73 | }; 74 | 75 | return dictResult; 76 | 77 | 78 | @app.route('/api/1.0/movies/group_by/') 79 | def get_movie_group_by(field): 80 | req = aggregations.AggregateRequest("*").group_by( 81 | "@" + field, 82 | reducers.count().alias("nb_of_movies") 83 | ).sort_by(aggregations.Asc("@" + field)).limit(0, 1000); 84 | 85 | res = conn.ft(index_name=redis_index).aggregate(req) 86 | 87 | reslist = [] 88 | for i in range(0, len(res.rows)-1): 89 | results = res.rows[i] 90 | interim_results = json.dumps(results) 91 | final_results = json.loads(interim_results) 92 | reslist.append(final_results); 93 | 94 | dictResult = jsonify(reslist, 200) 95 | 96 | return dictResult; 97 | 98 | 99 | @app.route('/api/1.0/movies/', methods=['POST', 'GET']) 100 | def get_movie_by_id(movie_id): 101 | dictResult = { 102 | "messsage": "This movie endpoint is not implemented in Java, use the Node.js Endpoint" 103 | }; 104 | return dictResult, 501; 105 | 106 | 107 | @app.route('/api/1.0/movies//comments', methods=['POST', 'GET']) 108 | def get_movie_comments(movie_id): 109 | dictResult = { 110 | "messsage": "Comment API not implemented in Python, use the Node.js Endpoint" 111 | }; 112 | return dictResult, 501; 113 | 114 | 115 | @app.route('/api/1.0/comments/', methods=['DELETE', 'GET']) 116 | def get_comment(movie_id): 117 | dictResult = { 118 | "messsage": "Comment API not implemented in Python, use the Node.js Endpoint" 119 | }; 120 | return dictResult, 501; 121 | 122 | 123 | def docs_to_dict(docs): 124 | reslist = [] 125 | for doc in docs: 126 | meta = {"id": getattr(doc, "id"), "score": getattr(doc, "score")} 127 | fields = {} 128 | for field in dir(doc): 129 | if (field.startswith('__') or field == 'id' or field == 'score'): 130 | continue 131 | fields.update({field: getattr(doc, field)}) 132 | ddict = {"meta": meta, "fields": fields}; 133 | reslist.append(ddict) 134 | return reslist 135 | 136 | 137 | if __name__ == "__main__": 138 | app.run(host="0.0.0.0", port=server_port); 139 | --------------------------------------------------------------------------------