0
# Index Operations
1
2
Enhanced index interface for querying data by secondary keys with full async iteration support.
3
4
## Capabilities
5
6
### Index Properties
7
8
Access index metadata and associated object store.
9
10
```typescript { .api }
11
/**
12
* The IDBObjectStore the index belongs to
13
*/
14
readonly objectStore: IDBPObjectStore<DBTypes, TxStores, StoreName, Mode>;
15
```
16
17
**Usage Examples:**
18
19
```typescript
20
const tx = db.transaction("users", "readonly");
21
const userStore = tx.store;
22
const emailIndex = userStore.index("email");
23
24
// Access parent object store
25
console.log("Parent store:", emailIndex.objectStore === userStore); // true
26
27
// Use parent store for operations not available on index
28
const user = await emailIndex.get("alice@example.com");
29
if (user) {
30
// Update user via parent store
31
const updateTx = db.transaction("users", "readwrite");
32
await updateTx.store.put({ ...user, lastLogin: new Date() });
33
await updateTx.done;
34
}
35
```
36
37
### Data Retrieval Operations
38
39
Query records using index keys instead of primary keys.
40
41
#### Get Operations
42
43
```typescript { .api }
44
/**
45
* Retrieves the value of the first record matching the query
46
* Resolves with undefined if no match is found
47
* @param query - Index key or key range to match
48
* @returns Promise resolving to the store value or undefined
49
*/
50
get(
51
query: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange
52
): Promise<StoreValue<DBTypes, StoreName> | undefined>;
53
54
/**
55
* Retrieves all values that match the query
56
* @param query - Index key or key range to match (optional)
57
* @param count - Maximum number of values to return (optional)
58
* @returns Promise resolving to array of matching store values
59
*/
60
getAll(
61
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
62
count?: number
63
): Promise<StoreValue<DBTypes, StoreName>[]>;
64
```
65
66
#### Key Operations
67
68
```typescript { .api }
69
/**
70
* Retrieves the key of the first record that matches the query
71
* Returns the primary key of the matching record
72
* Resolves with undefined if no match is found
73
* @param query - Index key or key range to match
74
* @returns Promise resolving to the primary store key or undefined
75
*/
76
getKey(
77
query: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange
78
): Promise<StoreKey<DBTypes, StoreName> | undefined>;
79
80
/**
81
* Retrieves the keys of records matching the query
82
* Returns primary keys of matching records
83
* @param query - Index key or key range to match (optional)
84
* @param count - Maximum number of keys to return (optional)
85
* @returns Promise resolving to array of primary store keys
86
*/
87
getAllKeys(
88
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
89
count?: number
90
): Promise<StoreKey<DBTypes, StoreName>[]>;
91
```
92
93
#### Count Operation
94
95
```typescript { .api }
96
/**
97
* Retrieves the number of records matching the given query
98
* @param key - Index key or key range to count (optional)
99
* @returns Promise resolving to the count
100
*/
101
count(
102
key?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null
103
): Promise<number>;
104
```
105
106
**Data Retrieval Examples:**
107
108
```typescript
109
interface UserDB {
110
users: {
111
key: number;
112
value: {
113
id: number;
114
name: string;
115
email: string;
116
age: number;
117
department: string;
118
tags: string[];
119
};
120
indexes: {
121
email: string;
122
name: string;
123
age: number;
124
department: string;
125
tags: string; // multiEntry index
126
};
127
};
128
}
129
130
const db = await openDB<UserDB>("company", 1, {
131
upgrade(db) {
132
const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true });
133
userStore.createIndex("email", "email", { unique: true });
134
userStore.createIndex("name", "name");
135
userStore.createIndex("age", "age");
136
userStore.createIndex("department", "department");
137
userStore.createIndex("tags", "tags", { multiEntry: true }); // Each tag creates separate index entry
138
}
139
});
140
141
const tx = db.transaction("users", "readonly");
142
const userStore = tx.store;
143
144
// Query by email (unique index)
145
const emailIndex = userStore.index("email");
146
const userByEmail = await emailIndex.get("alice@company.com");
147
if (userByEmail) {
148
console.log("Found user by email:", userByEmail.name);
149
}
150
151
// Query by age range
152
const ageIndex = userStore.index("age");
153
const youngEmployees = await ageIndex.getAll(IDBKeyRange.upperBound(30));
154
const seniorEmployees = await ageIndex.getAll(IDBKeyRange.lowerBound(50));
155
156
// Query by department
157
const deptIndex = userStore.index("department");
158
const engineeringTeam = await deptIndex.getAll("Engineering");
159
const firstFiveInSales = await deptIndex.getAll("Sales", 5);
160
161
// Query by tags (multiEntry index)
162
const tagIndex = userStore.index("tags");
163
const jsExperts = await tagIndex.getAll("javascript");
164
const fullStackDevs = await tagIndex.getAll("full-stack");
165
166
// Get primary keys only (more efficient)
167
const engineeringIds = await deptIndex.getAllKeys("Engineering");
168
const seniorIds = await ageIndex.getAllKeys(IDBKeyRange.lowerBound(50));
169
170
// Count operations
171
const totalUsers = await userStore.count();
172
const engineeringCount = await deptIndex.count("Engineering");
173
const adultCount = await ageIndex.count(IDBKeyRange.lowerBound(18));
174
175
await tx.done;
176
```
177
178
### Cursor Operations
179
180
Open cursors for efficient iteration over index results.
181
182
```typescript { .api }
183
/**
184
* Opens a cursor over the records matching the query
185
* Resolves with null if no matches are found
186
* @param query - Index key or key range to match (optional)
187
* @param direction - Cursor direction (next, nextunique, prev, prevunique)
188
* @returns Promise resolving to cursor with value or null
189
*/
190
openCursor(
191
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
192
direction?: IDBCursorDirection
193
): Promise<IDBPCursorWithValue<DBTypes, TxStores, StoreName, IndexName, Mode> | null>;
194
195
/**
196
* Opens a cursor over the keys matching the query
197
* Resolves with null if no matches are found
198
* @param query - Index key or key range to match (optional)
199
* @param direction - Cursor direction (next, nextunique, prev, prevunique)
200
* @returns Promise resolving to cursor or null
201
*/
202
openKeyCursor(
203
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
204
direction?: IDBCursorDirection
205
): Promise<IDBPCursor<DBTypes, TxStores, StoreName, IndexName, Mode> | null>;
206
```
207
208
**Cursor Usage Examples:**
209
210
```typescript
211
const tx = db.transaction("users", "readonly");
212
const userStore = tx.store;
213
const ageIndex = userStore.index("age");
214
215
// Manual cursor iteration
216
let cursor = await ageIndex.openCursor(IDBKeyRange.bound(25, 35));
217
while (cursor) {
218
console.log(`User ${cursor.value.name} is ${cursor.key} years old`);
219
cursor = await cursor.continue();
220
}
221
222
// Cursor with unique values only
223
let uniqueCursor = await ageIndex.openCursor(null, "nextunique");
224
while (uniqueCursor) {
225
console.log("Unique age:", cursor.key);
226
uniqueCursor = await uniqueCursor.continue();
227
}
228
229
// Key cursor (more efficient when you don't need full records)
230
let keyCursor = await ageIndex.openKeyCursor();
231
while (keyCursor) {
232
console.log("Age:", keyCursor.key, "Primary key:", keyCursor.primaryKey);
233
keyCursor = await keyCursor.continue();
234
}
235
```
236
237
### Async Iteration
238
239
Use modern async iteration syntax for convenient index traversal.
240
241
```typescript { .api }
242
/**
243
* Iterate over the index using async iteration
244
* @returns Async iterable iterator over cursor values
245
*/
246
[Symbol.asyncIterator](): AsyncIterableIterator<
247
IDBPCursorWithValueIteratorValue<DBTypes, TxStores, StoreName, IndexName, Mode>
248
>;
249
250
/**
251
* Iterate over the records matching the query
252
* @param query - Index key or key range to match (optional)
253
* @param direction - Cursor direction (optional)
254
* @returns Async iterable iterator over cursor values
255
*/
256
iterate(
257
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
258
direction?: IDBCursorDirection
259
): AsyncIterableIterator<
260
IDBPCursorWithValueIteratorValue<DBTypes, TxStores, StoreName, IndexName, Mode>
261
>;
262
```
263
264
**Async Iteration Examples:**
265
266
```typescript
267
const tx = db.transaction("users", "readonly");
268
const userStore = tx.store;
269
const deptIndex = userStore.index("department");
270
const ageIndex = userStore.index("age");
271
272
// Iterate over all records in department
273
for await (const cursor of deptIndex.iterate("Engineering")) {
274
console.log("Engineer:", cursor.value.name, "Age:", cursor.value.age);
275
276
// Skip junior engineers
277
if (cursor.value.age < 25) {
278
cursor.continue();
279
}
280
}
281
282
// Iterate over age range with early termination
283
let found = false;
284
for await (const cursor of ageIndex.iterate(IDBKeyRange.bound(30, 40))) {
285
if (cursor.value.name === "Target Employee") {
286
console.log("Found target employee at age", cursor.key);
287
found = true;
288
break;
289
}
290
}
291
292
// Collect filtered results
293
const seniorEngineers = [];
294
for await (const cursor of deptIndex.iterate("Engineering")) {
295
if (cursor.value.age >= 40) {
296
seniorEngineers.push(cursor.value);
297
}
298
}
299
300
// Iterate in reverse order
301
const newestEmployees = [];
302
for await (const cursor of ageIndex.iterate(null, "prev")) {
303
newestEmployees.push(cursor.value);
304
if (newestEmployees.length >= 10) break; // Get 10 youngest
305
}
306
307
await tx.done;
308
```
309
310
### Index Query Patterns
311
312
Common patterns for querying data using indexes.
313
314
**Exact Match Queries:**
315
316
```typescript
317
const tx = db.transaction("users", "readonly");
318
const emailIndex = tx.store.index("email");
319
const nameIndex = tx.store.index("name");
320
321
// Single exact match
322
const user = await emailIndex.get("alice@company.com");
323
324
// Multiple exact matches
325
const alices = await nameIndex.getAll("Alice");
326
```
327
328
**Range Queries:**
329
330
```typescript
331
const ageIndex = tx.store.index("age");
332
333
// Age ranges
334
const youngAdults = await ageIndex.getAll(IDBKeyRange.bound(18, 25));
335
const seniors = await ageIndex.getAll(IDBKeyRange.lowerBound(65));
336
const minors = await ageIndex.getAll(IDBKeyRange.upperBound(17));
337
338
// Exclude boundaries
339
const middleAged = await ageIndex.getAll(IDBKeyRange.bound(30, 50, true, true));
340
```
341
342
**Pattern Matching:**
343
344
```typescript
345
const nameIndex = tx.store.index("name");
346
347
// Names starting with "A"
348
const aNames = await nameIndex.getAll(IDBKeyRange.bound("A", "B", false, true));
349
350
// Names starting with "Alice"
351
const aliceVariations = await nameIndex.getAll(IDBKeyRange.bound("Alice", "Alicf", false, true));
352
```
353
354
**Multi-Entry Index Queries:**
355
356
```typescript
357
const tagIndex = tx.store.index("tags"); // multiEntry: true
358
359
// Users with specific skill
360
const jsDevs = await tagIndex.getAll("javascript");
361
const reactDevs = await tagIndex.getAll("react");
362
363
// Count users with skill
364
const jsDevCount = await tagIndex.count("javascript");
365
```
366
367
**Compound Queries (using multiple indexes):**
368
369
```typescript
370
// Find young engineers
371
const deptIndex = tx.store.index("department");
372
const ageIndex = tx.store.index("age");
373
374
// Get all engineers
375
const allEngineers = await deptIndex.getAll("Engineering");
376
377
// Filter by age in memory (for complex conditions)
378
const youngEngineers = allEngineers.filter(user => user.age < 30);
379
380
// Alternative: Use cursor for memory efficiency
381
const youngEngineersAlt = [];
382
for await (const cursor of deptIndex.iterate("Engineering")) {
383
if (cursor.value.age < 30) {
384
youngEngineersAlt.push(cursor.value);
385
}
386
}
387
```
388
389
### Performance Optimization
390
391
Strategies for efficient index operations.
392
393
**Choose Appropriate Index:**
394
395
```typescript
396
// Use most selective index first
397
const emailIndex = tx.store.index("email"); // Most selective (unique)
398
const deptIndex = tx.store.index("department"); // Less selective
399
const ageIndex = tx.store.index("age"); // Least selective
400
401
// Best: Start with most selective
402
const user = await emailIndex.get("specific@email.com");
403
404
// Good: Use department for departmental queries
405
const team = await deptIndex.getAll("Engineering");
406
407
// Acceptable: Use age for age-based queries
408
const adults = await ageIndex.getAll(IDBKeyRange.lowerBound(18));
409
```
410
411
**Key-Only Queries:**
412
413
```typescript
414
// More efficient when you only need to know if records exist
415
const hasEngineers = (await deptIndex.count("Engineering")) > 0;
416
417
// Get primary keys only
418
const engineerIds = await deptIndex.getAllKeys("Engineering");
419
420
// Instead of getting full records
421
const engineers = await deptIndex.getAll("Engineering");
422
const ids = engineers.map(e => e.id); // Less efficient
423
```
424
425
**Limit Result Sets:**
426
427
```typescript
428
// Use count parameter to limit results
429
const firstTenEngineers = await deptIndex.getAll("Engineering", 10);
430
431
// Use cursors for large datasets
432
let processed = 0;
433
for await (const cursor of deptIndex.iterate("Engineering")) {
434
processUser(cursor.value);
435
processed++;
436
437
if (processed >= 1000) break; // Process in batches
438
}
439
```