Indexing

      +

      Description — Couchbase Lite JavaScript — Indexing for Query Performance
      Related Content — SQL++ for Mobile | Query Resultsets | Live Queries

      Overview

      Indexes speed up queries by creating efficient lookup structures for your data. Without indexes, queries must scan every document in a collection, which becomes slow as your data grows.

      Couchbase Lite JavaScript uses IndexedDB’s native indexing capabilities, which provides fast query performance but has some unique constraints compared to native Couchbase Lite platforms.

      IndexedDB Indexing Constraints

      Due to IndexedDB requirements, indexes must be declared when opening the database. You cannot create or delete indexes while the database is open.

      Key Constraints:

      • Declaration at Open Time - All indexes must be specified in the DatabaseConfig when calling Database.open()

      • Limited Data Types - Only numbers, strings, and arrays of these types can be indexed

      • No Dynamic Index Management - Cannot add or remove indexes without closing and reopening the database

      • Per-Collection Indexes - Each collection has its own set of indexes

      Declaring Indexes

      Indexes are declared in the collection configuration when opening the database:

      Example 1. Basic index declaration
      const config: DatabaseConfig = {
          name: 'myapp',
          version: 1,
          collections: {
              products: {
                  indexes: ['name', 'price', 'category']
              }
          }
      };
      
      const db = await Database.open(config);

      Index Types

      Property Indexes

      The most common type of index is a property index, which indexes a single document property:

      Example 2. Property indexes
      const productConfig: DatabaseConfig = {
          name: 'store',
          version: 1,
          collections: {
              products: {
                  indexes: [
                      'sku',        // Product SKU
                      'price',      // Product price
                      'inStock'     // Stock status
                  ]
              }
          }
      };

      Array Indexes

      You can index array properties to enable efficient queries on array contents:

      Example 3. Array indexes
      const articleConfig: DatabaseConfig = {
          name: 'blog',
          version: 1,
          collections: {
              articles: {
                  indexes: [
                      'tags',       // Array of tags
                      'categories'  // Array of categories
                  ]
              }
          }
      };

      Nested Property Indexes

      Index nested object properties using dot notation:

      Example 4. Nested property indexes
      const userConfig: DatabaseConfig = {
          name: 'app',
          version: 1,
          collections: {
              users: {
                  indexes: [
                      'profile.name',
                      'profile.email',
                      'address.city',
                      'address.country'
                  ]
              }
          }
      };

      Compound Indexes

      While IndexedDB doesn’t support true compound indexes, you can create multiple single-property indexes that work together:

      Example 5. Multiple indexes
      const orderConfig: DatabaseConfig = {
          name: 'orders',
          version: 1,
          collections: {
              orders: {
                  indexes: [
                      'customerId',     // Filter by customer
                      'status',         // Filter by status
                      'orderDate',      // Sort by date
                      'totalAmount'     // Sort/filter by amount
                  ]
              }
          }
      };

      TypeScript Schema with Indexes

      When using TypeScript, declare both your document schema and indexes together:

      Example 6. Type-safe indexes
      interface Product {
          sku: string;
          name: string;
          price: number;
          category: string;
          tags: string[];
      }
      
      interface StoreSchema {
          products: Product;
      }
      
      // Configure with type-safe indexes
      const storeConfig: DatabaseConfig<StoreSchema> = {
          name: 'store',
          version: 1,
          collections: {
              products: {
                  indexes: ['sku', 'price', 'category', 'tags']
              }
          }
      };
      
      const storeDb = await Database.open(storeConfig);

      Modifying Indexes

      To add or remove indexes, you must close and reopen the database with the new configuration:

      Example 7. Modify indexes
      // Close database first
      await database.close();
      
      // Reopen with new indexes
      const updatedConfig: DatabaseConfig = {
          name: 'myapp',
          version: 2,  // Increment version
          collections: {
              products: {
                  indexes: [
                      'name',
                      'price',
                      'category',
                      'manufacturer'  // New index added
                  ]
              }
          }
      };
      
      const updatedDb = await Database.open(updatedConfig);

      How Queries Use Indexes

      Couchbase Lite automatically selects appropriate indexes for your queries. The query optimizer:

      1. Analyzes the WHERE clause

      2. Identifies indexed properties used in the query

      3. Selects the most efficient index

      4. Falls back to collection scanning if no suitable index exists

      Indexable Data Types

      IndexedDB can only index certain data types:

      Indexable Types:

      • Numbers - Integers and floating-point values

      • Strings - Text values

      • Arrays - Arrays containing numbers or strings

      Non-Indexable Types:

      • Objects (nested documents)

      • Booleans

      • Dates (must be converted to numbers or strings)

      • null values

      • Blobs

      Example 8. Handling dates in indexes
      interface Event {
          title: string;
          startDate: string;      // ISO date string: "2024-01-15T10:00:00Z"
          timestamp: number;      // Unix timestamp: 1705315200000
      }
      
      const eventConfig: DatabaseConfig = {
          name: 'events',
          version: 1,
          collections: {
              events: {
                  indexes: [
                      'startDate',    // Index ISO date string
                      'timestamp'     // Index numeric timestamp
                  ]
              }
          }
      };
      
      const database = await Database.open(eventConfig);

      Index Maintenance

      Automatic Updates:

      • Indexes update automatically when documents are saved

      • No manual maintenance required

      • Updates happen synchronously during document saves

      Performance Impact:

      • More indexes = slower write operations

      • Each indexed property adds overhead to saves

      • Balance query performance vs. write performance

      Query Optimization Strategies

      Use Indexes Effectively

      Structure queries to take advantage of indexes:

      Example 9. Indexed query
      const indexedQuery = database.createQuery(`
          SELECT *
          FROM products
          WHERE category = 'electronics' -- Uses category index
          ORDER BY price -- Uses price index
          LIMIT 50
      `);
      
      const results = await indexedQuery.execute();

      Avoid Full Collection Scans

      Queries without indexed properties scan the entire collection:

      Example 10. Unindexed query (slow)
      const unindexedQuery = database.createQuery(`
          SELECT *
          FROM products
          WHERE description LIKE '%wireless%' -- No index on description
      `);
      
      // This will be slow on large datasets
      const slowResults = await unindexedQuery.execute();

      Use LIMIT for Large Result Sets

      Even with indexes, limit results for better performance:

      Example 11. Limited query
      const limitedQuery = database.createQuery(`
          SELECT *
          FROM products
          WHERE category = $category
          ORDER BY price DESC LIMIT 20
          OFFSET $offset
      `);
      
      // Fetch first page
      limitedQuery.parameters = {
          category: 'electronics',
          offset: 0
      };
      const page1 = await limitedQuery.execute();
      
      // Fetch next page
      limitedQuery.parameters = {
          category: 'electronics',
          offset: 20
      };
      const page2 = await limitedQuery.execute();

      Common Indexing Patterns

      Email/Username Lookups

      Example 12. Email lookup pattern
      const authConfig: DatabaseConfig = {
          name: 'auth',
          version: 1,
          collections: {
              users: {
                  indexes: ['email', 'username']
              }
          }
      };
      
      // Fast lookup by email
      const userQuery = database.createQuery(`
          SELECT *
          FROM users
          WHERE email = $email
      `);
      
      userQuery.parameters = {email: 'user@example.com'};
      const user = await userQuery.execute();

      Status Filtering

      Example 13. Status filter pattern
      const workflowConfig: DatabaseConfig = {
          name: 'workflow',
          version: 1,
          collections: {
              tasks: {
                  indexes: ['status', 'priority', 'assignedTo']
              }
          }
      };
      
      // Query by status
      const activeTasksQuery = database.createQuery(`
          SELECT *
          FROM tasks
          WHERE status IN ('pending', 'in-progress')
            AND assignedTo = $userId
          ORDER BY priority DESC
      `);

      Date Range Queries

      Example 14. Date range pattern
      const analyticsConfig: DatabaseConfig = {
          name: 'analytics',
          version: 1,
          collections: {
              events: {
                  indexes: ['timestamp', 'eventType']
              }
          }
      };
      
      // Query date range
      const dateRangeQuery = database.createQuery(`
          SELECT *
          FROM events
          WHERE timestamp >= $startDate
            AND timestamp <= $endDate
          ORDER BY timestamp DESC
      `);
      
      dateRangeQuery.parameters = {
          startDate: Date.now() - (7 * 24 * 60 * 60 * 1000),  // 7 days ago
          endDate: Date.now()
      };
      const recentEvents = await dateRangeQuery.execute();

      Category Filtering

      Example 15. Category filter pattern
      const catalogConfig: DatabaseConfig = {
          name: 'catalog',
          version: 1,
          collections: {
              products: {
                  indexes: ['category', 'subcategory', 'brand']
              }
          }
      };
      
      // Hierarchical category query
      const categoryQuery = database.createQuery(`
          SELECT *
          FROM products
          WHERE category = $category
            AND subcategory = $subcategory
          ORDER BY name
      `);
      
      categoryQuery.parameters = {
          category: 'electronics',
          subcategory: 'laptops'
      };
      const laptops = await categoryQuery.execute();

      Indexing Limitations

      Missing Features:

      • No Full-Text Search Indexes - FTS with MATCH() not available

      • No Vector Indexes - Vector search without indexes is very slow

      • No Partial Indexes - Cannot index subset of documents

      • No Expression Indexes - Only property values can be indexed

      • No Multi-Property Compound Indexes - Each index is single-property

      Workarounds:

      • For FTS: Consider using a JavaScript FTS library and storing results

      • For vector search: Use EUCLIDEAN_DISTANCE() on small datasets only

      • For partial indexes: Filter in WHERE clause instead

      • For expression indexes: Store computed values as properties

      Performance Testing

      Always test index effectiveness with realistic data:

      Example 16. Performance testing pattern
      async function testQueryPerformance() {
          // Test without index
          const startUnindexed = performance.now();
          const unindexedResults = await database.createQuery(`
              SELECT *
              FROM products
              WHERE description LIKE '%test%'
          `).execute();
          const timeUnindexed = performance.now() - startUnindexed;
      
          console.log(`Unindexed query: ${timeUnindexed.toFixed(2)}ms`);
          console.log(`Results: ${unindexedResults.length}`);
      
          // Test with index
          const startIndexed = performance.now();
          const indexedResults = await database.createQuery(`
              SELECT *
              FROM products
              WHERE category = 'electronics'
          `).execute();
          const timeIndexed = performance.now() - startIndexed;
      
          console.log(`Indexed query: ${timeIndexed.toFixed(2)}ms`);
          console.log(`Results: ${indexedResults.length}`);
          console.log(`Speedup: ${(timeUnindexed / timeIndexed).toFixed(1)}x`);
      }

      Debugging Index Issues

      Query Not Using Index:

      1. Verify property name matches index exactly

      2. Ensure indexed property is used in WHERE clause

      3. Check that query condition is compatible with indexing

      Slow Queries Despite Indexes:

      1. Check index selectivity (too many duplicate values?)

      2. Verify result set size (use LIMIT if appropriate)

      3. Monitor IndexedDB performance in browser DevTools

      4. Consider simplifying complex queries

      Index Declaration Errors:

      1. Verify indexes declared in DatabaseConfig

      2. Check property names for typos

      3. Ensure database version increments when changing indexes

      4. Verify database successfully reopened with new config

      Index Migration Strategy

      When adding indexes to existing databases:

      Example 17. Index migration
      async function migrateIndexes() {
          // Step 1: Check current version
          const currentDb = await Database.open({
              name: 'myapp',
              version: 1,
              collections: {
                  products: {
                      indexes: ['name']
                  }
              }
          });
      
          console.log('Current version:', currentDb.config.version);
      
          // Step 2: Close database
          await currentDb.close();
      
          // Step 3: Reopen with new indexes and incremented version
          const migratedDb = await Database.open({
              name: 'myapp',
              version: 2,
              collections: {
                  products: {
                      indexes: [
                          'name',
                          'price',      // New index
                          'category'    // New index
                      ]
                  }
              }
          });
      
          console.log('Migrated to version:', migratedDb.config.version);
      
          return migratedDb;
      }
      
      await migrateIndexes();

      How to . . .

      .

      Dive Deeper . . .

      Mobile Forum | Blog | Tutorials

      .