redis-om-type 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG +168 -0
- package/LICENSE +22 -0
- package/README.md +1150 -0
- package/dist/index.d.ts +1398 -0
- package/dist/index.js +2047 -0
- package/docs/.nojekyll +1 -0
- package/docs/README.md +479 -0
- package/docs/classes/AbstractSearch.md +912 -0
- package/docs/classes/ArrayHashInput.md +198 -0
- package/docs/classes/Circle.md +375 -0
- package/docs/classes/Client.md +190 -0
- package/docs/classes/Field.md +254 -0
- package/docs/classes/FieldNotInSchema.md +198 -0
- package/docs/classes/InvalidHashInput.md +213 -0
- package/docs/classes/InvalidHashValue.md +228 -0
- package/docs/classes/InvalidInput.md +207 -0
- package/docs/classes/InvalidJsonInput.md +228 -0
- package/docs/classes/InvalidJsonValue.md +228 -0
- package/docs/classes/InvalidSchema.md +197 -0
- package/docs/classes/InvalidValue.md +203 -0
- package/docs/classes/NestedHashInput.md +198 -0
- package/docs/classes/NullJsonInput.md +228 -0
- package/docs/classes/NullJsonValue.md +228 -0
- package/docs/classes/PointOutOfRange.md +203 -0
- package/docs/classes/RawSearch.md +1063 -0
- package/docs/classes/RedisOmError.md +207 -0
- package/docs/classes/Repository.md +425 -0
- package/docs/classes/Schema.md +242 -0
- package/docs/classes/Search.md +1199 -0
- package/docs/classes/SearchError.md +201 -0
- package/docs/classes/SemanticSearchError.md +197 -0
- package/docs/classes/Where.md +43 -0
- package/docs/classes/WhereField.md +915 -0
- package/logo.svg +1 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<br/>
|
|
3
|
+
<br/>
|
|
4
|
+
<img width="360" src="logo.svg" alt="Redis OM" />
|
|
5
|
+
<br/>
|
|
6
|
+
<br/>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
TypeScript-based Node.js library for Redis with object mapping and additional utilities.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
[![Discord][discord-shield]][discord-url]
|
|
16
|
+
[![Twitch][twitch-shield]][twitch-url]
|
|
17
|
+
[![YouTube][youtube-shield]][youtube-url]
|
|
18
|
+
[![Twitter][twitter-shield]][twitter-url]
|
|
19
|
+
|
|
20
|
+
[![NPM][package-shield]][package-url]
|
|
21
|
+
[![Build][build-shield]][build-url]
|
|
22
|
+
[![License][license-shield]][license-url]
|
|
23
|
+
|
|
24
|
+
**redis-om-type** implements schemas, repositories, and fluent search over Redis. It follows the same model as [Redis OM for Node.js](https://github.com/redis/redis-om-node), with TypeScript declarations and tooling for Node.js applications.
|
|
25
|
+
|
|
26
|
+
Related Redis OM clients: [Redis OM .NET][redis-om-dotnet] · [Redis OM Node.js (upstream)](https://github.com/redis/redis-om-node) · [Redis OM Python][redis-om-python] · [Redis OM Spring][redis-om-spring]
|
|
27
|
+
|
|
28
|
+
<details>
|
|
29
|
+
<summary><strong>Table of contents</strong></summary>
|
|
30
|
+
|
|
31
|
+
- [Overview](#overview)
|
|
32
|
+
- [Getting Started](#getting-started)
|
|
33
|
+
- [Connect to Redis with Node Redis](#connect-to-redis-with-node-redis)
|
|
34
|
+
- [Redis Connection Strings](#redis-connection-strings)
|
|
35
|
+
- [Entities and Schemas](#entities-and-schemas)
|
|
36
|
+
- [JSON and Hashes](#json-and-hashes)
|
|
37
|
+
- [Configuring JSON](#configuring-json)
|
|
38
|
+
- [Configuring Hashes](#configuring-hashes)
|
|
39
|
+
- [Reading, Writing, and Removing with Repository](#reading-writing-and-removing-with-repository)
|
|
40
|
+
- [Creating Entities](#creating-entities)
|
|
41
|
+
- [Missing Entities and Null Values](#missing-entities-and-null-values)
|
|
42
|
+
- [Searching](#searching)
|
|
43
|
+
- [Build the Index](#build-the-index)
|
|
44
|
+
- [Finding All The Things (and Returning Them)](#finding-all-the-things-and-returning-them)
|
|
45
|
+
- [Pagination](#pagination)
|
|
46
|
+
- [First Things First](#first-things-first)
|
|
47
|
+
- [Counting](#counting)
|
|
48
|
+
- [Finding Specific Things](#finding-specific-things)
|
|
49
|
+
- [Searching on Strings](#searching-on-strings)
|
|
50
|
+
- [Searching on Numbers](#searching-on-numbers)
|
|
51
|
+
- [Searching on Booleans](#searching-on-booleans)
|
|
52
|
+
- [Searching on Dates](#searching-on-dates)
|
|
53
|
+
- [Searching String Arrays](#searching-string-arrays)
|
|
54
|
+
- [Searching Arrays of Numbers](#searching-arrays-of-numbers)
|
|
55
|
+
- [Full-Text Search](#full-text-search)
|
|
56
|
+
- [Searching on Points](#searching-on-points)
|
|
57
|
+
- [Chaining Searches](#chaining-searches)
|
|
58
|
+
- [Running Raw Searches](#running-raw-searches)
|
|
59
|
+
- [Sorting Search Results](#sorting-search-results)
|
|
60
|
+
- [Advanced usage](#advanced-usage)
|
|
61
|
+
- [Schema Options](#schema-options)
|
|
62
|
+
- [Documentation](#documentation)
|
|
63
|
+
- [Troubleshooting](#troubleshooting)
|
|
64
|
+
- [Contributing](#contributing)
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
## Overview
|
|
68
|
+
|
|
69
|
+
Redis OM (often pronounced “redis-ohm”) maps Redis data to plain JavaScript objects so you can persist and query entities without hand-writing low-level commands. The API is fluent and composable, with strong TypeScript support in this package.
|
|
70
|
+
|
|
71
|
+
Define a schema:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const schema = new Schema('album', {
|
|
75
|
+
artist: { type: 'string' },
|
|
76
|
+
title: { type: 'text' },
|
|
77
|
+
year: { type: 'number' }
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Create a JavaScript object and save it:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
const album = {
|
|
85
|
+
artist: "Mushroomhead",
|
|
86
|
+
title: "The Righteous & The Butterfly",
|
|
87
|
+
year: 2014
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await repository.save(album)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Search for matching entities:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const albums = await repository.search()
|
|
97
|
+
.where('artist').equals('Mushroomhead')
|
|
98
|
+
.and('title').matches('butterfly')
|
|
99
|
+
.and('year').is.greaterThan(2000)
|
|
100
|
+
.return.all()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The sections below describe schemas, repositories, and search in full.
|
|
104
|
+
|
|
105
|
+
> ## Migrating from Redis OM 0.3.x
|
|
106
|
+
>
|
|
107
|
+
> Version 0.4 introduced breaking changes relative to 0.3.6. If you are new to this API, follow this README from top to bottom.
|
|
108
|
+
>
|
|
109
|
+
> If you already use **redis-om** on npm, read this document and [CHANGELOG](CHANGELOG) before upgrading. The [redis-om 0.3.6 README](https://www.npmjs.com/package/redis-om/v/0.3.6) remains available for comparison. Later 0.4.x releases add requested improvements; further non-breaking changes are expected.
|
|
110
|
+
|
|
111
|
+
## Getting Started
|
|
112
|
+
|
|
113
|
+
Create or use an existing Node.js project (for example `npm init`).
|
|
114
|
+
|
|
115
|
+
Install this package and the Redis client:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install redis-om-type redis
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use [Redis Stack][redis-stack-url] (includes [RediSearch][redisearch-url] and [RedisJSON][redis-json-url]) or [Redis Cloud][redis-cloud-url]. With Docker:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Connect to Redis with Node Redis
|
|
128
|
+
|
|
129
|
+
Before using **redis-om-type**, connect to Redis with [Node Redis](https://github.com/redis/node-redis). Minimal setup (from the Node Redis [README](https://github.com/redis/node-redis)):
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
import { createClient } from 'redis'
|
|
133
|
+
|
|
134
|
+
const redis = createClient()
|
|
135
|
+
redis.on('error', (err) => console.log('Redis Client Error', err));
|
|
136
|
+
await redis.connect()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For advanced connection options, clustering, and tuning, see the Node Redis [documentation](https://github.com/redis/node-redis).
|
|
140
|
+
|
|
141
|
+
With a connected client you can run ordinary Redis commands when needed:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
|
|
145
|
+
const aString = await redis.ping() // 'PONG'
|
|
146
|
+
const aNumber = await redis.hSet('foo', 'alfa', '42', 'bravo', '23') // 2
|
|
147
|
+
const aHash = await redis.hGetAll('foo') // { alfa: '42', bravo: '23' }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
When you are finished with a connection, close it with `.quit`:
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
await redis.quit()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Redis Connection Strings
|
|
157
|
+
|
|
158
|
+
By default, Node Redis connects to `localhost:6379`. Override this with a URL:
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
const redis = createClient({ url: 'redis://alice:foobared@awesome.redis.server:6380' })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The basic format for this URL is:
|
|
165
|
+
|
|
166
|
+
redis://username:password@host:port
|
|
167
|
+
|
|
168
|
+
The full URI scheme is [registered with IANA](https://www.iana.org/assignments/uri-schemes/prov/redis); TLS uses [`rediss://`](https://www.iana.org/assignments/uri-schemes/prov/rediss).
|
|
169
|
+
|
|
170
|
+
For sockets, clustering, and other options, see the Node Redis [client configuration](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md) and [clustering](https://github.com/redis/node-redis/blob/master/docs/clustering.md) guides.
|
|
171
|
+
|
|
172
|
+
## Entities and Schemas
|
|
173
|
+
|
|
174
|
+
This library is concerned with saving, reading, and deleting *entities*. An [Entity](docs/README.md#entity) is plain JavaScript object data you persist or load from Redis. Almost any object shape can serve as an `Entity`.
|
|
175
|
+
|
|
176
|
+
[Schemas](docs/classes/Schema.md) declare which fields an entity may have: each field’s type, how it is stored in Redis, and how it participates in RediSearch when indexing is enabled. By default, entities are stored as JSON (RedisJSON); you can opt into Redis Hashes instead (see below).
|
|
177
|
+
|
|
178
|
+
Define a `Schema` as follows:
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
import { Schema } from 'redis-om-type'
|
|
182
|
+
|
|
183
|
+
const albumSchema = new Schema('album', {
|
|
184
|
+
artist: { type: 'string' },
|
|
185
|
+
title: { type: 'text' },
|
|
186
|
+
year: { type: 'number' },
|
|
187
|
+
genres: { type: 'string[]' },
|
|
188
|
+
songDurations: { type: 'number[]' },
|
|
189
|
+
outOfPublication: { type: 'boolean' }
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const studioSchema = new Schema('studio', {
|
|
193
|
+
name: { type: 'string' },
|
|
194
|
+
city: { type: 'string' },
|
|
195
|
+
state: { type: 'string' },
|
|
196
|
+
location: { type: 'point' },
|
|
197
|
+
established: { type: 'date' }
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The first argument is the schema name. It becomes part of the Redis key prefix for entities using this schema. Choose a name that is unique on your Redis instance and meaningful to your domain (here, `album` and `studio`).
|
|
202
|
+
|
|
203
|
+
The second argument lists fields that may appear on entities. Object keys are the names you use in queries; each field’s `type` must be one of: `string`, `number`, `boolean`, `string[]`, `number[]`, `date`, `point`, or `text`.
|
|
204
|
+
|
|
205
|
+
The first three types do exactly what you think—they define a field that is a [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), a [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), or a [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). `string[]` and `number[]` do what you'd think as well, specifically describing an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of Strings or Numbers respectively.
|
|
206
|
+
|
|
207
|
+
`date` is a little different, but still more or less what you'd expect. It describes a property that contains a [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) and can be set using not only a Date but also a String containing an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date or a number with the [UNIX epoch time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps) in *seconds* (NOTE: the JavaScript Date object is specified in *milliseconds*).
|
|
208
|
+
|
|
209
|
+
A `point` defines a point somewhere on the globe as a longitude and a latitude. It is expressed as a simple object with `longitude` and `latitude` properties. Like this:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
const point = { longitude: 12.34, latitude: 56.78 }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
A `text` field behaves like a `string` for plain read and write operations. For search, the two diverge: `string` fields support exact and tag-style matching (good for codes and identifiers), while `text` fields use RediSearch full-text indexing ([stemming](https://redis.io/docs/stack/search/reference/stemming/), [stop words](https://redis.io/docs/stack/search/reference/stopwords/), and related features). See [Searching](#searching).
|
|
216
|
+
|
|
217
|
+
### JSON and Hashes
|
|
218
|
+
|
|
219
|
+
By default, entities are stored as JSON documents (RedisJSON). You can set this explicitly:
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
const albumSchema = new Schema('album', {
|
|
223
|
+
artist: { type: 'string' },
|
|
224
|
+
title: { type: 'string' },
|
|
225
|
+
year: { type: 'number' },
|
|
226
|
+
genres: { type: 'string[]' },
|
|
227
|
+
songDurations: { type: 'number[]' },
|
|
228
|
+
outOfPublication: { type: 'boolean' }
|
|
229
|
+
}, {
|
|
230
|
+
dataStructure: 'JSON'
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
To store entities as Redis Hashes, set `dataStructure` to `'HASH'`:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
const albumSchema = new Schema('album', {
|
|
238
|
+
artist: { type: 'string' },
|
|
239
|
+
title: { type: 'string' },
|
|
240
|
+
year: { type: 'number' },
|
|
241
|
+
genres: { type: 'string[]' },
|
|
242
|
+
outOfPublication: { type: 'boolean' }
|
|
243
|
+
}, {
|
|
244
|
+
dataStructure: 'HASH'
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Hashes and JSON differ: hashes are flat field/value maps; JSON documents are trees and may be nested. That affects how you configure paths and fields.
|
|
249
|
+
|
|
250
|
+
> Hash-backed schemas cannot use `number[]`; that type is JSON-only. Omit it from hash schemas or use JSON.
|
|
251
|
+
|
|
252
|
+
#### Configuring JSON
|
|
253
|
+
|
|
254
|
+
When you store your entities as JSON, the path to the properties in your JSON document and your JavaScript object default to the name of your property in the schema. In the above example, this would result in a document that looks like this:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"artist": "Mushroomhead",
|
|
259
|
+
"title": "The Righteous & The Butterfly",
|
|
260
|
+
"year": 2014,
|
|
261
|
+
"genres": [ "metal" ],
|
|
262
|
+
"songDurations": [ 204, 290, 196, 210, 211, 105, 244, 245, 209, 252, 259, 200, 215, 219 ],
|
|
263
|
+
"outOfPublication": true
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
However, you might not want your JavaScript object and your JSON to map this way. So, you can provide a `path` option in your schema that contains a [JSONPath](https://redis.io/docs/stack/json/path/#jsonpath-syntax) pointing to where that field *actually* exists in the JSON and your entity. For example, we might want to store some of the album's data inside of an album property like this:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"album": {
|
|
272
|
+
"artist": "Mushroomhead",
|
|
273
|
+
"title": "The Righteous & The Butterfly",
|
|
274
|
+
"year": 2014,
|
|
275
|
+
"genres": [ "metal" ],
|
|
276
|
+
"songDurations": [ 204, 290, 196, 210, 211, 105, 244, 245, 209, 252, 259, 200, 215, 219 ]
|
|
277
|
+
},
|
|
278
|
+
"outOfPublication": true
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
To do this, we'll need to specify the `path` property for the nested fields in the schema:
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
const albumSchema = new Schema('album', {
|
|
286
|
+
artist: { type: 'string', path: '$.album.artist' },
|
|
287
|
+
title: { type: 'string', path: '$.album.title' },
|
|
288
|
+
year: { type: 'number', path: '$.album.year' },
|
|
289
|
+
genres: { type: 'string[]', path: '$.album.genres[*]' },
|
|
290
|
+
songDurations: { type: 'number[]', path: '$.album.songDurations[*]' },
|
|
291
|
+
outOfPublication: { type: 'boolean' }
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
There are two things to note here:
|
|
296
|
+
|
|
297
|
+
1. We haven't specified a path for `outOfPublication` as it's still in the root of the document. It defaults to `$.outOfPublication`.
|
|
298
|
+
2. Our `genres` field points to a `string[]`. When using a `string[]` the JSONPath must return an array. If it doesn't, an error will be generated.
|
|
299
|
+
3. Same for our `songDurations`.
|
|
300
|
+
|
|
301
|
+
#### Configuring Hashes
|
|
302
|
+
|
|
303
|
+
When you store your entities as Hashes there is no nesting—all the entities are flat. In Redis, the properties on your entity are stored in fields inside a Hash. The default name for each field is the name of the property in your schema and this is the name that will be used in your entities. So, for the following schema:
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const albumSchema = new Schema('album', {
|
|
307
|
+
artist: { type: 'string' },
|
|
308
|
+
title: { type: 'string' },
|
|
309
|
+
year: { type: 'number' },
|
|
310
|
+
genres: { type: 'string[]' },
|
|
311
|
+
outOfPublication: { type: 'boolean' }
|
|
312
|
+
}, {
|
|
313
|
+
dataStructure: 'HASH'
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
In your code, your entities would look like this:
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
{
|
|
321
|
+
artist: 'Mushroomhead',
|
|
322
|
+
title: 'The Righteous & The Butterfly',
|
|
323
|
+
year: 2014,
|
|
324
|
+
genres: [ 'metal' ],
|
|
325
|
+
outOfPublication: true
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Inside Redis, your Hash would be stored like this:
|
|
330
|
+
|
|
331
|
+
| Field | Value |
|
|
332
|
+
|------------------|:----------------------------------------|
|
|
333
|
+
| artist | Mushroomhead |
|
|
334
|
+
| title | The Righteous & The Butterfly |
|
|
335
|
+
| year | 2014 |
|
|
336
|
+
| genres | metal |
|
|
337
|
+
| outOfPublication | 1 |
|
|
338
|
+
|
|
339
|
+
However, you might not want the names of your fields and the names of the properties on your entity to be exactly the same. Maybe you've got some existing data with existing names or something.
|
|
340
|
+
|
|
341
|
+
Fear not! You can change the name of the field used by Redis with the `field` property:
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
const albumSchema = new Schema('album', {
|
|
345
|
+
artist: { type: 'string', field: 'album_artist' },
|
|
346
|
+
title: { type: 'string', field: 'album_title' },
|
|
347
|
+
year: { type: 'number', field: 'album_year' },
|
|
348
|
+
genres: { type: 'string[]' },
|
|
349
|
+
outOfPublication: { type: 'boolean' }
|
|
350
|
+
}, {
|
|
351
|
+
dataStructure: 'HASH'
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
With this configuration, your entities will remain unchanged and will still have properties for `artist`, `title`, `year`, `genres`, and `outOfPublication`. But inside Redis, the field will have changed:
|
|
356
|
+
|
|
357
|
+
| Field | Value |
|
|
358
|
+
|------------------|:----------------------------------------|
|
|
359
|
+
| album_artist | Mushroomhead |
|
|
360
|
+
| album_title | The Righteous & The Butterfly |
|
|
361
|
+
| album_year | 2014 |
|
|
362
|
+
| genres | metal |
|
|
363
|
+
| outOfPublication | 1 |
|
|
364
|
+
|
|
365
|
+
## Reading, Writing, and Removing with Repository
|
|
366
|
+
|
|
367
|
+
Given a connected Redis client and a schema, create a [*repository*](docs/classes/Repository.md) to create, read, update, delete, and search entities:
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
import { Repository } from 'redis-om-type'
|
|
371
|
+
|
|
372
|
+
const albumRepository = new Repository(albumSchema, redis)
|
|
373
|
+
const studioRepository = new Repository(studioSchema, redis)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Call `.save` to persist entities:
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
let album = {
|
|
380
|
+
artist: "Mushroomhead",
|
|
381
|
+
title: "The Righteous & The Butterfly",
|
|
382
|
+
year: 2014,
|
|
383
|
+
genres: [ 'metal' ],
|
|
384
|
+
songDurations: [ 204, 290, 196, 210, 211, 105, 244, 245, 209, 252, 259, 200, 215, 219 ],
|
|
385
|
+
outOfPublication: true
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
album = await albumRepository.save(album)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`.save` returns the persisted entity, including a generated entity ID. The ID is not a normal string property (to avoid collisions with your own fields); it is exposed through a [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). Import `EntityId` from **redis-om-type** and read the ID with `entity[EntityId]`:
|
|
392
|
+
|
|
393
|
+
```javascript
|
|
394
|
+
import { EntityId } from 'redis-om-type'
|
|
395
|
+
|
|
396
|
+
album = await albumRepository.save(album)
|
|
397
|
+
album[EntityId] // '01FJYWEYRHYFT8YTEGQBABJ43J'
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Generated IDs are [ULIDs](https://github.com/ulid/spec). To use your own ID, pass it as the first argument to `.save`:
|
|
401
|
+
|
|
402
|
+
```javascript
|
|
403
|
+
album = await albumRepository.save('BWOMP', album)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Load an entity by ID with `.fetch`:
|
|
407
|
+
|
|
408
|
+
```javascript
|
|
409
|
+
const album = await albumRepository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')
|
|
410
|
+
album.artist // "Mushroomhead"
|
|
411
|
+
album.title // "The Righteous & The Butterfly"
|
|
412
|
+
album.year // 2014
|
|
413
|
+
album.genres // [ 'metal' ]
|
|
414
|
+
album.songDurations // [ 204, 290, 196, 210, 211, 105, 244, 245, 209, 252, 259, 200, 215, 219 ]
|
|
415
|
+
album.outOfPublication // true
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
If the entity already has an ID (for example after a `.fetch`), `.save` updates the existing record:
|
|
419
|
+
|
|
420
|
+
```javascript
|
|
421
|
+
let album = await albumRepository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')
|
|
422
|
+
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
|
|
423
|
+
album.outOfPublication = false
|
|
424
|
+
|
|
425
|
+
album = await albumRepository.save(album)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
To copy data to a new ID, call `.save` with a new entity ID as the first argument:
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
const album = await albumRepository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')
|
|
432
|
+
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
|
|
433
|
+
album.outOfPublication = false
|
|
434
|
+
|
|
435
|
+
const clonedEntity = await albumRepository.save('BWOMP', album)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Delete entities with `.remove`:
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
await albumRepository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Set a TTL in seconds with `.expire` (Redis removes the key when it elapses):
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
const ttlInSeconds = 12 * 60 * 60 // 12 hours
|
|
448
|
+
await albumRepository.expire('01FJYWEYRHYFT8YTEGQBABJ43J', ttlInSeconds)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Missing Entities and Null Values
|
|
452
|
+
|
|
453
|
+
Redis (and this library) do not fully distinguish “missing field” from “null,” especially for hashes. Fetching a non-existent key still yields an object whose fields are empty and whose `[EntityId]` is the ID you requested:
|
|
454
|
+
|
|
455
|
+
```javascript
|
|
456
|
+
const album = await albumRepository.fetch('TOTALLY_BOGUS')
|
|
457
|
+
album[EntityId] // 'TOTALLY_BOGUS'
|
|
458
|
+
album.artist // undefined
|
|
459
|
+
album.title // undefined
|
|
460
|
+
album.year // undefined
|
|
461
|
+
album.genres // undefined
|
|
462
|
+
album.outOfPublication // undefined
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
If you strip all schema fields and save, the key may be removed from Redis:
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
const album = await albumRepository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')
|
|
469
|
+
delete album.artist
|
|
470
|
+
delete album.title
|
|
471
|
+
delete album.year
|
|
472
|
+
delete album.genres
|
|
473
|
+
delete album.outOfPublication
|
|
474
|
+
|
|
475
|
+
const entityId = await albumRepository.save(album)
|
|
476
|
+
|
|
477
|
+
const exists = await redis.exists('album:01FJYWEYRHYFT8YTEGQBABJ43J') // 0
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Because of that, `.fetch` always returns an object shape; treat empty fields as “no data” for that ID.
|
|
481
|
+
|
|
482
|
+
## Searching
|
|
483
|
+
|
|
484
|
+
With [RediSearch][redisearch-url] available on your server, repositories support indexed search—for example:
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
const albums = await albumRepository.search()
|
|
488
|
+
.where('artist').equals('Mushroomhead')
|
|
489
|
+
.and('title').matches('butterfly')
|
|
490
|
+
.and('year').is.greaterThan(2000)
|
|
491
|
+
.return.all()
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Build the Index
|
|
495
|
+
|
|
496
|
+
Search requires a RediSearch index. Create (or update) it with `.createIndex` on the repository:
|
|
497
|
+
|
|
498
|
+
```javascript
|
|
499
|
+
await albumRepository.createIndex();
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
After schema changes, call `.createIndex` again; the library rebuilds the index only when the schema definition changes, so it is safe to invoke during application startup.
|
|
503
|
+
|
|
504
|
+
Rebuilding can be slow on large datasets—plan that in deployment scripts if needed. To drop an index without immediately rebuilding, use `.dropIndex`:
|
|
505
|
+
|
|
506
|
+
```javascript
|
|
507
|
+
await albumRepository.dropIndex();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Finding All The Things (and Returning Them)
|
|
511
|
+
|
|
512
|
+
Return every entity matching the (possibly empty) query:
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
const albums = await albumRepository.search().return.all()
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### Pagination
|
|
519
|
+
|
|
520
|
+
Request a slice with a zero-based offset and page size:
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
const offset = 100
|
|
524
|
+
const count = 25
|
|
525
|
+
const albums = await albumRepository.search().return.page(offset, count)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
If the offset is past the end of the result set, you get an empty array.
|
|
529
|
+
|
|
530
|
+
#### First Things First
|
|
531
|
+
|
|
532
|
+
Return the first match, or `null` if none:
|
|
533
|
+
|
|
534
|
+
```javascript
|
|
535
|
+
const firstAlbum = await albumRepository.search().return.first();
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### Counting
|
|
539
|
+
|
|
540
|
+
Return the number of matches without loading documents:
|
|
541
|
+
|
|
542
|
+
```javascript
|
|
543
|
+
const count = await albumRepository.search().return.count()
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Finding Specific Things
|
|
547
|
+
|
|
548
|
+
Narrow results using [strings](#searching-on-strings), [numbers](#searching-on-numbers), [booleans](#searching-on-booleans), [string arrays](#searching-string-arrays), [numeric arrays](#searching-arrays-of-numbers), [full-text](#full-text-search), [dates](#searching-on-dates), and [geo radius](#searching-on-points). The fluent API offers several equivalent phrasings for the same predicate; the sections below list common patterns.
|
|
549
|
+
|
|
550
|
+
#### Searching on Strings
|
|
551
|
+
|
|
552
|
+
When you set the field type in your schema to `string`, you can search for a particular value in that string. You can also search for partial strings (no shorter than two characters) that occur at the beginning, middle, or end of a string. If you need to search strings in a more sophisticated manner, you'll want to look at the `text` type and search it using the [Full-Text Search](#full-text-search) syntax.
|
|
553
|
+
|
|
554
|
+
```javascript
|
|
555
|
+
let albums
|
|
556
|
+
|
|
557
|
+
// find all albums where the artist is 'Mushroomhead'
|
|
558
|
+
albums = await albumRepository.search().where('artist').eq('Mushroomhead').return.all()
|
|
559
|
+
|
|
560
|
+
// find all albums where the artist is *not* 'Mushroomhead'
|
|
561
|
+
albums = await albumRepository.search().where('artist').not.eq('Mushroomhead').return.all()
|
|
562
|
+
|
|
563
|
+
// find all albums using wildcards
|
|
564
|
+
albums = await albumRepository.search().where('artist').eq('Mush*').return.all()
|
|
565
|
+
albums = await albumRepository.search().where('artist').eq('*head').return.all()
|
|
566
|
+
albums = await albumRepository.search().where('artist').eq('*room*').return.all()
|
|
567
|
+
|
|
568
|
+
// fluent alternatives that do the same thing
|
|
569
|
+
albums = await albumRepository.search().where('artist').equals('Mushroomhead').return.all()
|
|
570
|
+
albums = await albumRepository.search().where('artist').does.equal('Mushroomhead').return.all()
|
|
571
|
+
albums = await albumRepository.search().where('artist').is.equalTo('Mushroomhead').return.all()
|
|
572
|
+
albums = await albumRepository.search().where('artist').does.not.equal('Mushroomhead').return.all()
|
|
573
|
+
albums = await albumRepository.search().where('artist').is.not.equalTo('Mushroomhead').return.all()
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
#### Searching on Numbers
|
|
577
|
+
|
|
578
|
+
When you set the field type in your schema to `number`, you can store both integers and floating-point numbers. And you can search against it with all the comparisons you'd expect to see:
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
let albums
|
|
582
|
+
|
|
583
|
+
// find all albums where the year is ===, >, >=, <, and <= 1984
|
|
584
|
+
albums = await albumRepository.search().where('year').eq(1984).return.all()
|
|
585
|
+
albums = await albumRepository.search().where('year').gt(1984).return.all()
|
|
586
|
+
albums = await albumRepository.search().where('year').gte(1984).return.all()
|
|
587
|
+
albums = await albumRepository.search().where('year').lt(1984).return.all()
|
|
588
|
+
albums = await albumRepository.search().where('year').lte(1984).return.all()
|
|
589
|
+
|
|
590
|
+
// find all albums where the year is between 1980 and 1989 inclusive
|
|
591
|
+
albums = await albumRepository.search().where('year').between(1980, 1989).return.all()
|
|
592
|
+
|
|
593
|
+
// find all albums where the year is *not* ===, >, >=, <, and <= 1984
|
|
594
|
+
albums = await albumRepository.search().where('year').not.eq(1984).return.all()
|
|
595
|
+
albums = await albumRepository.search().where('year').not.gt(1984).return.all()
|
|
596
|
+
albums = await albumRepository.search().where('year').not.gte(1984).return.all()
|
|
597
|
+
albums = await albumRepository.search().where('year').not.lt(1984).return.all()
|
|
598
|
+
albums = await albumRepository.search().where('year').not.lte(1984).return.all()
|
|
599
|
+
|
|
600
|
+
// find all albums where year is *not* between 1980 and 1989 inclusive
|
|
601
|
+
albums = await albumRepository.search().where('year').not.between(1980, 1989);
|
|
602
|
+
|
|
603
|
+
// fluent alternatives that do the same thing
|
|
604
|
+
albums = await albumRepository.search().where('year').equals(1984).return.all()
|
|
605
|
+
albums = await albumRepository.search().where('year').does.equal(1984).return.all()
|
|
606
|
+
albums = await albumRepository.search().where('year').does.not.equal(1984).return.all()
|
|
607
|
+
albums = await albumRepository.search().where('year').is.equalTo(1984).return.all()
|
|
608
|
+
albums = await albumRepository.search().where('year').is.not.equalTo(1984).return.all()
|
|
609
|
+
|
|
610
|
+
albums = await albumRepository.search().where('year').greaterThan(1984).return.all()
|
|
611
|
+
albums = await albumRepository.search().where('year').is.greaterThan(1984).return.all()
|
|
612
|
+
albums = await albumRepository.search().where('year').is.not.greaterThan(1984).return.all()
|
|
613
|
+
|
|
614
|
+
albums = await albumRepository.search().where('year').greaterThanOrEqualTo(1984).return.all()
|
|
615
|
+
albums = await albumRepository.search().where('year').is.greaterThanOrEqualTo(1984).return.all()
|
|
616
|
+
albums = await albumRepository.search().where('year').is.not.greaterThanOrEqualTo(1984).return.all()
|
|
617
|
+
|
|
618
|
+
albums = await albumRepository.search().where('year').lessThan(1984).return.all()
|
|
619
|
+
albums = await albumRepository.search().where('year').is.lessThan(1984).return.all()
|
|
620
|
+
albums = await albumRepository.search().where('year').is.not.lessThan(1984).return.all()
|
|
621
|
+
|
|
622
|
+
albums = await albumRepository.search().where('year').lessThanOrEqualTo(1984).return.all()
|
|
623
|
+
albums = await albumRepository.search().where('year').is.lessThanOrEqualTo(1984).return.all()
|
|
624
|
+
albums = await albumRepository.search().where('year').is.not.lessThanOrEqualTo(1984).return.all()
|
|
625
|
+
|
|
626
|
+
albums = await albumRepository.search().where('year').is.between(1980, 1989).return.all()
|
|
627
|
+
albums = await albumRepository.search().where('year').is.not.between(1980, 1989).return.all()
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
#### Searching on Booleans
|
|
631
|
+
|
|
632
|
+
You can search against fields that contain booleans if you defined a field type of `boolean` in your schema:
|
|
633
|
+
|
|
634
|
+
```javascript
|
|
635
|
+
let albums
|
|
636
|
+
|
|
637
|
+
// find all albums where outOfPublication is true
|
|
638
|
+
albums = await albumRepository.search().where('outOfPublication').true().return.all()
|
|
639
|
+
|
|
640
|
+
// find all albums where outOfPublication is false
|
|
641
|
+
albums = await albumRepository.search().where('outOfPublication').false().return.all()
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
You can negate boolean searches. This might seem odd, but if your field is `null`, then it would match on a `.not` query:
|
|
645
|
+
|
|
646
|
+
```javascript
|
|
647
|
+
// find all albums where outOfPublication is false or null
|
|
648
|
+
albums = await albumRepository.search().where('outOfPublication').not.true().return.all()
|
|
649
|
+
|
|
650
|
+
// find all albums where outOfPublication is true or null
|
|
651
|
+
albums = await albumRepository.search().where('outOfPublication').not.false().return.all()
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
And, of course, there's lots of syntactic sugar to make this fluent:
|
|
655
|
+
|
|
656
|
+
```javascript
|
|
657
|
+
albums = await albumRepository.search().where('outOfPublication').eq(true).return.all()
|
|
658
|
+
albums = await albumRepository.search().where('outOfPublication').equals(true).return.all()
|
|
659
|
+
albums = await albumRepository.search().where('outOfPublication').does.equal(true).return.all()
|
|
660
|
+
albums = await albumRepository.search().where('outOfPublication').is.equalTo(true).return.all()
|
|
661
|
+
|
|
662
|
+
albums = await albumRepository.search().where('outOfPublication').true().return.all()
|
|
663
|
+
albums = await albumRepository.search().where('outOfPublication').false().return.all()
|
|
664
|
+
albums = await albumRepository.search().where('outOfPublication').is.true().return.all()
|
|
665
|
+
albums = await albumRepository.search().where('outOfPublication').is.false().return.all()
|
|
666
|
+
|
|
667
|
+
albums = await albumRepository.search().where('outOfPublication').not.eq(true).return.all()
|
|
668
|
+
albums = await albumRepository.search().where('outOfPublication').does.not.equal(true).return.all()
|
|
669
|
+
albums = await albumRepository.search().where('outOfPublication').is.not.equalTo(true).return.all()
|
|
670
|
+
albums = await albumRepository.search().where('outOfPublication').is.not.true().return.all()
|
|
671
|
+
albums = await albumRepository.search().where('outOfPublication').is.not.false().return.all()
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
#### Searching on Dates
|
|
675
|
+
|
|
676
|
+
If you have a field type of `date` in your schema, you can search on it using [Dates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date), [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted strings, or the [UNIX epoch time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps) in *seconds*:
|
|
677
|
+
|
|
678
|
+
```javascript
|
|
679
|
+
studios = await studioRepository.search().where('established').on(new Date('2010-12-27')).return.all()
|
|
680
|
+
studios = await studioRepository.search().where('established').on('2010-12-27').return.all()
|
|
681
|
+
studios = await studioRepository.search().where('established').on(1293408000).return.all()
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
There are several date comparison methods to use. And they can be negated:
|
|
685
|
+
|
|
686
|
+
```javascript
|
|
687
|
+
const date = new Date('2010-12-27')
|
|
688
|
+
const laterDate = new Date('2020-12-27')
|
|
689
|
+
|
|
690
|
+
studios = await studioRepository.search().where('established').on(date).return.all()
|
|
691
|
+
studios = await studioRepository.search().where('established').not.on(date).return.all()
|
|
692
|
+
studios = await studioRepository.search().where('established').before(date).return.all()
|
|
693
|
+
studios = await studioRepository.search().where('established').not.before(date).return.all()
|
|
694
|
+
studios = await studioRepository.search().where('established').after(date).return.all()
|
|
695
|
+
studios = await studioRepository.search().where('established').not.after(date).return.all()
|
|
696
|
+
studios = await studioRepository.search().where('established').onOrBefore(date).return.all()
|
|
697
|
+
studios = await studioRepository.search().where('established').not.onOrBefore(date).return.all()
|
|
698
|
+
studios = await studioRepository.search().where('established').onOrAfter(date).return.all()
|
|
699
|
+
studios = await studioRepository.search().where('established').not.onOrAfter(date).return.all()
|
|
700
|
+
studios = await studioRepository.search().where('established').between(date, laterDate).return.all()
|
|
701
|
+
studios = await studioRepository.search().where('established').not.between(date, laterDate).return.all()
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
More fluent variations work too:
|
|
705
|
+
|
|
706
|
+
```javascript
|
|
707
|
+
const date = new Date('2010-12-27')
|
|
708
|
+
const laterDate = new Date('2020-12-27')
|
|
709
|
+
|
|
710
|
+
studios = await studioRepository.search().where('established').is.on(date).return.all()
|
|
711
|
+
studios = await studioRepository.search().where('established').is.not.on(date).return.all()
|
|
712
|
+
|
|
713
|
+
studios = await studioRepository.search().where('established').is.before(date).return.all()
|
|
714
|
+
studios = await studioRepository.search().where('established').is.not.before(date).return.all()
|
|
715
|
+
|
|
716
|
+
studios = await studioRepository.search().where('established').is.onOrBefore(date).return.all()
|
|
717
|
+
studios = await studioRepository.search().where('established').is.not.onOrBefore(date).return.all()
|
|
718
|
+
|
|
719
|
+
studios = await studioRepository.search().where('established').is.after(date).return.all()
|
|
720
|
+
studios = await studioRepository.search().where('established').is.not.after(date).return.all()
|
|
721
|
+
|
|
722
|
+
studios = await studioRepository.search().where('established').is.onOrAfter(date).return.all()
|
|
723
|
+
studios = await studioRepository.search().where('established').is.not.onOrAfter(date).return.all()
|
|
724
|
+
|
|
725
|
+
studios = await studioRepository.search().where('established').is.between(date, laterDate).return.all()
|
|
726
|
+
studios = await studioRepository.search().where('established').is.not.between(date, laterDate).return.all()
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
And, since dates are really just numbers, all the numeric comparisons work too:
|
|
730
|
+
|
|
731
|
+
```javascript
|
|
732
|
+
const date = new Date('2010-12-27')
|
|
733
|
+
const laterDate = new Date('2020-12-27')
|
|
734
|
+
|
|
735
|
+
studios = await studioRepository.search().where('established').eq(date).return.all()
|
|
736
|
+
studios = await studioRepository.search().where('established').not.eq(date).return.all()
|
|
737
|
+
studios = await studioRepository.search().where('established').equals(date).return.all()
|
|
738
|
+
studios = await studioRepository.search().where('established').does.equal(date).return.all()
|
|
739
|
+
studios = await studioRepository.search().where('established').does.not.equal(date).return.all()
|
|
740
|
+
studios = await studioRepository.search().where('established').is.equalTo(date).return.all()
|
|
741
|
+
studios = await studioRepository.search().where('established').is.not.equalTo(date).return.all()
|
|
742
|
+
|
|
743
|
+
studios = await studioRepository.search().where('established').gt(date).return.all()
|
|
744
|
+
studios = await studioRepository.search().where('established').not.gt(date).return.all()
|
|
745
|
+
studios = await studioRepository.search().where('established').greaterThan(date).return.all()
|
|
746
|
+
studios = await studioRepository.search().where('established').is.greaterThan(date).return.all()
|
|
747
|
+
studios = await studioRepository.search().where('established').is.not.greaterThan(date).return.all()
|
|
748
|
+
|
|
749
|
+
studios = await studioRepository.search().where('established').gte(date).return.all()
|
|
750
|
+
studios = await studioRepository.search().where('established').not.gte(date).return.all()
|
|
751
|
+
studios = await studioRepository.search().where('established').greaterThanOrEqualTo(date).return.all()
|
|
752
|
+
studios = await studioRepository.search().where('established').is.greaterThanOrEqualTo(date).return.all()
|
|
753
|
+
studios = await studioRepository.search().where('established').is.not.greaterThanOrEqualTo(date).return.all()
|
|
754
|
+
|
|
755
|
+
studios = await studioRepository.search().where('established').lt(date).return.all()
|
|
756
|
+
studios = await studioRepository.search().where('established').not.lt(date).return.all()
|
|
757
|
+
studios = await studioRepository.search().where('established').lessThan(date).return.all()
|
|
758
|
+
studios = await studioRepository.search().where('established').is.lessThan(date).return.all()
|
|
759
|
+
studios = await studioRepository.search().where('established').is.not.lessThan(date).return.all()
|
|
760
|
+
|
|
761
|
+
studios = await studioRepository.search().where('established').lte(date).return.all()
|
|
762
|
+
studios = await studioRepository.search().where('established').not.lte(date).return.all()
|
|
763
|
+
studios = await studioRepository.search().where('established').lessThanOrEqualTo(date).return.all()
|
|
764
|
+
studios = await studioRepository.search().where('established').is.lessThanOrEqualTo(date).return.all()
|
|
765
|
+
studios = await studioRepository.search().where('established').is.not.lessThanOrEqualTo(date).return.all()
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
#### Searching String Arrays
|
|
769
|
+
|
|
770
|
+
If you have a field type of `string[]` you can search for *whole strings* that are in that array:
|
|
771
|
+
|
|
772
|
+
```javascript
|
|
773
|
+
let albums
|
|
774
|
+
|
|
775
|
+
// find all albums where genres contains the string 'rock'
|
|
776
|
+
albums = await albumRepository.search().where('genres').contain('rock').return.all()
|
|
777
|
+
|
|
778
|
+
// find all albums where genres contains the string 'rock', 'metal', or 'blues'
|
|
779
|
+
albums = await albumRepository.search().where('genres').containOneOf('rock', 'metal', 'blues').return.all()
|
|
780
|
+
|
|
781
|
+
// find all albums where genres does *not* contain the string 'rock'
|
|
782
|
+
albums = await albumRepository.search().where('genres').not.contain('rock').return.all()
|
|
783
|
+
|
|
784
|
+
// find all albums where genres does *not* contain the string 'rock', 'metal', and 'blues'
|
|
785
|
+
albums = await albumRepository.search().where('genres').not.containOneOf('rock', 'metal', 'blues').return.all()
|
|
786
|
+
|
|
787
|
+
// alternative syntaxes
|
|
788
|
+
albums = await albumRepository.search().where('genres').contains('rock').return.all()
|
|
789
|
+
albums = await albumRepository.search().where('genres').containsOneOf('rock', 'metal', 'blues').return.all()
|
|
790
|
+
albums = await albumRepository.search().where('genres').does.contain('rock').return.all()
|
|
791
|
+
albums = await albumRepository.search().where('genres').does.not.contain('rock').return.all()
|
|
792
|
+
albums = await albumRepository.search().where('genres').does.containOneOf('rock', 'metal', 'blues').return.all()
|
|
793
|
+
albums = await albumRepository.search().where('genres').does.not.containOneOf('rock', 'metal', 'blues').return.all()
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Wildcards work here too:
|
|
797
|
+
|
|
798
|
+
```javascript
|
|
799
|
+
albums = await albumRepository.search().where('genres').contain('*rock*').return.all()
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### Searching Arrays of Numbers
|
|
803
|
+
|
|
804
|
+
If you have a field of type `number[]`, you can search on it just like a `number`. If any number in the array matches your criteria, then it'll match and the document will be returned.
|
|
805
|
+
|
|
806
|
+
```javascript
|
|
807
|
+
let albums
|
|
808
|
+
|
|
809
|
+
// find all albums where at least one song is at least 3 minutes long
|
|
810
|
+
albums = await albumRepository.search().where('songDurations').gte(180).return.all()
|
|
811
|
+
|
|
812
|
+
// find all albums where at least one song is at exactly 3 minutes long
|
|
813
|
+
albums = await albumRepository.search().where('songDurations').eq(180).return.all()
|
|
814
|
+
|
|
815
|
+
// find all albums where at least one song is between 3 and 4 minutes long
|
|
816
|
+
albums = await albumRepository.search().where('songDurations').between(180, 240).return.all()
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
The same numeric predicates as in [Searching on numbers](#searching-on-numbers) apply to `number[]` fields (any array element may satisfy the predicate).
|
|
820
|
+
|
|
821
|
+
#### Full-Text Search
|
|
822
|
+
|
|
823
|
+
If you've defined a field with a type of `text` in your schema, you can store text in it and perform full-text searches against it. Full-text search is different from how a `string` is searched. With full-text search, you can look for words, partial words, fuzzy matches, and exact phrases within a body of text.
|
|
824
|
+
|
|
825
|
+
Full-text search treats natural language: common words (*a*, *an*, *the*, …) may be skipped; related forms (*give*, *gives*, *given*) may match via stemming; punctuation and extra whitespace are normalized.
|
|
826
|
+
|
|
827
|
+
Here are some examples of doing full-text search against some album titles:
|
|
828
|
+
|
|
829
|
+
```javascript
|
|
830
|
+
let albums
|
|
831
|
+
|
|
832
|
+
// finds all albums where the title contains the word 'butterfly'
|
|
833
|
+
albums = await albumRepository.search().where('title').match('butterfly').return.all()
|
|
834
|
+
|
|
835
|
+
// finds all albums using fuzzy matching where the title contains a word which is within 3 Levenshtein distance of the word 'buterfly'
|
|
836
|
+
albums = await albumRepository.search().where('title').match('buterfly', { fuzzyMatching: true, levenshteinDistance: 3 }).return.all()
|
|
837
|
+
|
|
838
|
+
// finds all albums where the title contains the words 'beautiful' and 'children'
|
|
839
|
+
albums = await albumRepository.search().where('title').match('beautiful children').return.all()
|
|
840
|
+
|
|
841
|
+
// finds all albums where the title contains the exact phrase 'beautiful stories'
|
|
842
|
+
albums = await albumRepository.search().where('title').matchExact('beautiful stories').return.all()
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
For prefix, suffix, or infix word fragments, add `*` where needed:
|
|
846
|
+
|
|
847
|
+
```javascript
|
|
848
|
+
// finds all albums where the title contains a word that contains 'right'
|
|
849
|
+
albums = await albumRepository.search().where('title').match('*right*').return.all()
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Do not combine partial-word searches or fuzzy matches with exact matches. Partial-word searches and fuzzy matches with exact matches are not compatible in RediSearch. If you try to exactly match a partial-word search or fuzzy match a partial-word search, you'll get an error.
|
|
853
|
+
|
|
854
|
+
```javascript
|
|
855
|
+
// THESE WILL ERROR
|
|
856
|
+
albums = await albumRepository.search().where('title').matchExact('beautiful sto*').return.all()
|
|
857
|
+
albums = await albumRepository.search().where('title').matchExact('*buterfly', { fuzzyMatching: true, levenshteinDistance: 3 }).return.all()
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
As always, there are several alternatives to make this a bit more fluent and, of course, negation is available:
|
|
861
|
+
|
|
862
|
+
```javascript
|
|
863
|
+
albums = await albumRepository.search().where('title').not.match('butterfly').return.all()
|
|
864
|
+
albums = await albumRepository.search().where('title').matches('butterfly').return.all()
|
|
865
|
+
albums = await albumRepository.search().where('title').does.match('butterfly').return.all()
|
|
866
|
+
albums = await albumRepository.search().where('title').does.not.match('butterfly').return.all()
|
|
867
|
+
|
|
868
|
+
albums = await albumRepository.search().where('title').exact.match('beautiful stories').return.all()
|
|
869
|
+
albums = await albumRepository.search().where('title').not.exact.match('beautiful stories').return.all()
|
|
870
|
+
albums = await albumRepository.search().where('title').exactly.matches('beautiful stories').return.all()
|
|
871
|
+
albums = await albumRepository.search().where('title').does.exactly.match('beautiful stories').return.all()
|
|
872
|
+
albums = await albumRepository.search().where('title').does.not.exactly.match('beautiful stories').return.all()
|
|
873
|
+
|
|
874
|
+
albums = await albumRepository.search().where('title').not.matchExact('beautiful stories').return.all()
|
|
875
|
+
albums = await albumRepository.search().where('title').matchesExactly('beautiful stories').return.all()
|
|
876
|
+
albums = await albumRepository.search().where('title').does.matchExactly('beautiful stories').return.all()
|
|
877
|
+
albums = await albumRepository.search().where('title').does.not.matchExactly('beautiful stories').return.all()
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
#### Searching on Points
|
|
881
|
+
|
|
882
|
+
RediSearch supports geographic queries: supply a point and a radius to return entities within that area:
|
|
883
|
+
|
|
884
|
+
```javascript
|
|
885
|
+
let studios
|
|
886
|
+
|
|
887
|
+
// finds all the studios with 50 miles of downtown Cleveland
|
|
888
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
889
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).miles).return.all()
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
Note that coordinates are specified with the longitude *first*, and then the latitude. This might be the opposite of what you expect but is consistent with how Redis implements coordinates in [RediSearch](https://oss.redis.com/redisearch/Query_Syntax/) and with [GeoSets](https://redis.io/commands#geo).
|
|
893
|
+
|
|
894
|
+
If you don't want to rely on argument order, you can also specify longitude and latitude more explicitly:
|
|
895
|
+
|
|
896
|
+
```javascript
|
|
897
|
+
// finds all the studios within 50 miles of downtown Cleveland using a point
|
|
898
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
899
|
+
circle => circle.origin({ longitude: -81.7758995, latitude: 41.4976393 }).radius(50).miles).return.all()
|
|
900
|
+
|
|
901
|
+
// finds all the studios within 50 miles of downtown Cleveland using longitude and latitude
|
|
902
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
903
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Radius can be in *miles*, *feet*, *kilometers*, and *meters* in all the spelling variations you could ever want:
|
|
907
|
+
|
|
908
|
+
```javascript
|
|
909
|
+
// finds all the studios within 50 miles
|
|
910
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
911
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).miles).return.all()
|
|
912
|
+
|
|
913
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
914
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).mile).return.all()
|
|
915
|
+
|
|
916
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
917
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).mi).return.all()
|
|
918
|
+
|
|
919
|
+
// finds all the studios within 50 feet
|
|
920
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
921
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).feet).return.all()
|
|
922
|
+
|
|
923
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
924
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).foot).return.all()
|
|
925
|
+
|
|
926
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
927
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).ft).return.all()
|
|
928
|
+
|
|
929
|
+
// finds all the studios within 50 kilometers
|
|
930
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
931
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).kilometers).return.all()
|
|
932
|
+
|
|
933
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
934
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).kilometer).return.all()
|
|
935
|
+
|
|
936
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
937
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).km).return.all()
|
|
938
|
+
|
|
939
|
+
// finds all the studios within 50 meters
|
|
940
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
941
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).meters).return.all()
|
|
942
|
+
|
|
943
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
944
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).meter).return.all()
|
|
945
|
+
|
|
946
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
947
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50).m).return.all()
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
If you omit the origin, the library defaults to longitude `0` and latitude `0` ([Null Island](https://en.wikipedia.org/wiki/Null_Island)):
|
|
951
|
+
|
|
952
|
+
```javascript
|
|
953
|
+
// finds all the studios within 50 miles of Null Island (probably ain't much there)
|
|
954
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
955
|
+
circle => circle.radius(50).miles).return.all()
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
If you don't specify the radius, it defaults to *1* and if you don't provide units, it defaults to *meters*:
|
|
959
|
+
|
|
960
|
+
```javascript
|
|
961
|
+
// finds all the studios within 1 meter of downtown Cleveland
|
|
962
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
963
|
+
circle => circle.origin(-81.7758995, 41.4976393)).return.all()
|
|
964
|
+
|
|
965
|
+
// finds all the studios within 1 kilometer of downtown Cleveland
|
|
966
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
967
|
+
circle => circle.origin(-81.7758995, 41.4976393).kilometers).return.all()
|
|
968
|
+
|
|
969
|
+
// finds all the studios within 50 meters of downtown Cleveland
|
|
970
|
+
studios = await studioRepository.search().where('location').inRadius(
|
|
971
|
+
circle => circle.origin(-81.7758995, 41.4976393).radius(50)).return.all()
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
Equivalent fluent spellings include:
|
|
975
|
+
|
|
976
|
+
```javascript
|
|
977
|
+
studios = await studioRepository.search().where('location').not.inRadius(
|
|
978
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
979
|
+
|
|
980
|
+
studios = await studioRepository.search().where('location').is.inRadius(
|
|
981
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
982
|
+
|
|
983
|
+
studios = await studioRepository.search().where('location').is.not.inRadius(
|
|
984
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
985
|
+
|
|
986
|
+
studios = await studioRepository.search().where('location').not.inCircle(
|
|
987
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
988
|
+
|
|
989
|
+
studios = await studioRepository.search().where('location').is.inCircle(
|
|
990
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
991
|
+
|
|
992
|
+
studios = await studioRepository.search().where('location').is.not.inCircle(
|
|
993
|
+
circle => circle.longitude(-81.7758995).latitude(41.4976393).radius(50).miles).return.all()
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
#### Chaining Searches
|
|
997
|
+
|
|
998
|
+
So far we've been doing searches that match on a single field. However, we often want to query on multiple fields. Not a problem:
|
|
999
|
+
|
|
1000
|
+
```javascript
|
|
1001
|
+
const albums = await albumRepository.search()
|
|
1002
|
+
.where('artist').equals('Mushroomhead')
|
|
1003
|
+
.or('title').matches('butterfly')
|
|
1004
|
+
.and('year').is.greaterThan(1990).return.all()
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
These are executed in order from left to right, and ignore any order of operations. So this query will match an artist of "Mushroomhead" OR a title matching "butterfly" before it goes on to match that the year is greater than 1990.
|
|
1008
|
+
|
|
1009
|
+
If you'd like to change this you can nest your queries:
|
|
1010
|
+
|
|
1011
|
+
```javascript
|
|
1012
|
+
const albums = await albumRepository.search()
|
|
1013
|
+
.or(search => search
|
|
1014
|
+
.where('artist').equals('Mushroomhead')
|
|
1015
|
+
.and('year').is.greaterThan(1990)
|
|
1016
|
+
)
|
|
1017
|
+
.or(search => search.where('title').matches('butterfly'))
|
|
1018
|
+
.return.all()
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
This matches albums where `(artist is Mushroomhead and year > 1990)` **or** the title matches `butterfly`. Adjust grouping with nested callbacks if you need different precedence; see [Chaining Searches](#chaining-searches).
|
|
1022
|
+
|
|
1023
|
+
#### Running Raw Searches
|
|
1024
|
+
|
|
1025
|
+
For queries that are easier to express in RediSearch’s native syntax, use `.searchRaw` with a query string. See the [RediSearch query syntax](https://oss.redis.com/redisearch/Query_Syntax/).
|
|
1026
|
+
|
|
1027
|
+
```javascript
|
|
1028
|
+
// finds all the Mushroomhead albums with the word 'beautiful' in the title from 1990 and beyond
|
|
1029
|
+
const query = "@artist:{Mushroomhead} @title:beautiful @year:[1990 +inf]"
|
|
1030
|
+
const albums = albumRepository.searchRaw(query).return.all();
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
Results are the same entity objects as with the fluent builder.
|
|
1034
|
+
|
|
1035
|
+
### Sorting Search Results
|
|
1036
|
+
|
|
1037
|
+
Sort by a single field with `.sortBy`, `.sortAscending`, or `.sortDescending` on these types: `string`, `number`, `boolean`, `date`, and `text`.
|
|
1038
|
+
|
|
1039
|
+
```javascript
|
|
1040
|
+
const albumsByYear = await albumRepository.search()
|
|
1041
|
+
.where('artist').equals('Mushroomhead')
|
|
1042
|
+
.sortAscending('year').return.all()
|
|
1043
|
+
|
|
1044
|
+
const albumsByTitle = await albumRepository.search()
|
|
1045
|
+
.where('artist').equals('Mushroomhead')
|
|
1046
|
+
.sortBy('title', 'DESC').return.all()
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
Mark fields as `sortable` in the schema so RediSearch can maintain a sortable side index where supported (not every field type benefits):
|
|
1050
|
+
|
|
1051
|
+
```javascript
|
|
1052
|
+
const albumSchema = new Schema('album', {
|
|
1053
|
+
artist: { type: 'string' },
|
|
1054
|
+
title: { type: 'text', sortable: true },
|
|
1055
|
+
year: { type: 'number', sortable: true },
|
|
1056
|
+
genres: { type: 'string[]' },
|
|
1057
|
+
outOfPublication: { type: 'boolean' }
|
|
1058
|
+
})
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
If your schema is for a JSON data structure (the default), you can mark `number`, `date`, and `text` fields as sortable. You can also mark `string` and `boolean` fields as sortable, but this will have no effect and will generate a warning.
|
|
1062
|
+
|
|
1063
|
+
If your schema is for a Hash, you can mark `string`, `number`, `boolean`, `date`, and `text` fields as sortable.
|
|
1064
|
+
|
|
1065
|
+
Fields of the types `point` and `string[]` are never sortable.
|
|
1066
|
+
|
|
1067
|
+
Calling `.sortBy` on a field that could be `sortable` but is not may produce a runtime warning.
|
|
1068
|
+
|
|
1069
|
+
## Advanced usage
|
|
1070
|
+
|
|
1071
|
+
Additional configuration for RediSearch-backed schemas:
|
|
1072
|
+
|
|
1073
|
+
### Schema Options
|
|
1074
|
+
|
|
1075
|
+
Additional field options can be set depending on the field type. These correspond to the [Field Options](https://redis.io/commands/ft.create/#field-options) available when creating a RediSearch full-text index. Other than the `separator` option, these only affect how content is indexed and searched.
|
|
1076
|
+
|
|
1077
|
+
| schema type | RediSearch type | `indexed` | `sortable` | `normalized` | `stemming` | `matcher` | `weight` | `separator` | `caseSensitive` |
|
|
1078
|
+
| -------------- | :-------------: | :-------: | :--------: | :----------: | :--------: | :--------: | :------: | :---------: | :-------------: |
|
|
1079
|
+
| `string` | TAG | yes | HASH Only | HASH Only | - | - | - | yes | yes |
|
|
1080
|
+
| `number` | NUMERIC | yes | yes | - | - | - | - | - | - |
|
|
1081
|
+
| `boolean` | TAG | yes | HASH Only | - | - | - | - | - | - |
|
|
1082
|
+
| `string[]` | TAG | yes | HASH Only | HASH Only | - | - | - | yes | yes |
|
|
1083
|
+
| `number[]` | NUMERIC | yes | yes | - | - | - | - | - | - |
|
|
1084
|
+
| `date` | NUMERIC | yes | yes | - | | - | - | - | - |
|
|
1085
|
+
| `point` | GEO | yes | - | - | | - | - | - | - |
|
|
1086
|
+
| `text` | TEXT | yes | yes | yes | yes | yes | yes | - | - |
|
|
1087
|
+
|
|
1088
|
+
* `indexed`: true | false, whether this field is indexed by RediSearch (default true)
|
|
1089
|
+
* `sortable`: true | false, whether to create an additional index to optimize sorting (default false)
|
|
1090
|
+
* `normalized`: true | false, whether to apply normalization for sorting (default true)
|
|
1091
|
+
* `matcher`: string defining phonetic matcher which can be one of: 'dm:en' for English, 'dm:fr' for French, 'dm:pt' for Portuguese, 'dm:es' for Spanish (default none)
|
|
1092
|
+
* `stemming`: true | false, whether word-stemming is applied to text fields (default true)
|
|
1093
|
+
* `weight`: number, the importance weighting to use when ranking results (default 1)
|
|
1094
|
+
* `separator`: string, the character to delimit multiple tags (default '|')
|
|
1095
|
+
* `caseSensitive`: true | false, whether original letter casing is kept for search (default false)
|
|
1096
|
+
|
|
1097
|
+
Example showing additional options:
|
|
1098
|
+
|
|
1099
|
+
```javascript
|
|
1100
|
+
const commentSchema = new Schema('comment', {
|
|
1101
|
+
name: { type: 'text', stemming: false, matcher: 'dm:en' },
|
|
1102
|
+
email: { type: 'string', normalized: false, },
|
|
1103
|
+
posted: { type: 'date', sortable: true },
|
|
1104
|
+
title: { type: 'text', weight: 2 },
|
|
1105
|
+
comment: { type: 'text', weight: 1 },
|
|
1106
|
+
approved: { type: 'boolean', indexed: false },
|
|
1107
|
+
iphash: { type: 'string', caseSensitive: true },
|
|
1108
|
+
notes: { type: 'string', indexed: false },
|
|
1109
|
+
})
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
See [docs/classes/Schema.md](docs/classes/Schema.md) for additional `Schema` options.
|
|
1113
|
+
|
|
1114
|
+
## Documentation
|
|
1115
|
+
|
|
1116
|
+
Generated TypeDoc output lives under [`docs/`](docs/). This README covers day-to-day usage; the class reference fills in edge cases and full method signatures.
|
|
1117
|
+
|
|
1118
|
+
## Troubleshooting
|
|
1119
|
+
|
|
1120
|
+
If something fails, confirm Redis Stack (RediSearch + RedisJSON) is available, indices are created, and your schema matches stored data. For upstream **redis-om** behavior, see issues on [redis-om-node](https://github.com/redis/redis-om-node). Community help is available on the [Redis Discord][discord-url].
|
|
1121
|
+
|
|
1122
|
+
## Contributing
|
|
1123
|
+
|
|
1124
|
+
Contributions are welcome: reproducible bug reports, documentation fixes, and pull requests that include or update tests when behavior changes.
|
|
1125
|
+
|
|
1126
|
+
<!-- Links, Badges, and Whatnot -->
|
|
1127
|
+
|
|
1128
|
+
[package-shield]: https://img.shields.io/npm/v/redis-om-type?logo=npm
|
|
1129
|
+
[build-shield]: https://img.shields.io/github/workflow/status/redis/redis-om-node/CI/main
|
|
1130
|
+
[license-shield]: https://img.shields.io/npm/l/redis-om-type
|
|
1131
|
+
[discord-shield]: https://img.shields.io/discord/697882427875393627?style=social&logo=discord
|
|
1132
|
+
[twitch-shield]: https://img.shields.io/twitch/status/redisinc?style=social
|
|
1133
|
+
[twitter-shield]: https://img.shields.io/twitter/follow/redisinc?style=social
|
|
1134
|
+
[youtube-shield]: https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social
|
|
1135
|
+
|
|
1136
|
+
[package-url]: https://www.npmjs.com/package/redis-om-type
|
|
1137
|
+
[build-url]: https://github.com/redis/redis-om-node/actions/workflows/ci.yml
|
|
1138
|
+
[license-url]: LICENSE
|
|
1139
|
+
[discord-url]: http://discord.gg/redis
|
|
1140
|
+
[twitch-url]: https://www.twitch.tv/redisinc
|
|
1141
|
+
[twitter-url]: https://twitter.com/redisinc
|
|
1142
|
+
[youtube-url]: https://www.youtube.com/redisinc
|
|
1143
|
+
|
|
1144
|
+
[redis-cloud-url]: https://redis.com/try-free/
|
|
1145
|
+
[redis-stack-url]: https://redis.io/docs/stack/
|
|
1146
|
+
[redisearch-url]: https://oss.redis.com/redisearch/
|
|
1147
|
+
[redis-json-url]: https://oss.redis.com/redisjson/
|
|
1148
|
+
[redis-om-dotnet]: https://github.com/redis/redis-om-dotnet
|
|
1149
|
+
[redis-om-python]: https://github.com/redis/redis-om-python
|
|
1150
|
+
[redis-om-spring]: https://github.com/redis/redis-om-spring
|