Menu

Advanced Queries Examples

This guide demonstrates advanced querying techniques in Adonis ODM, including complex aggregations, text search, geospatial queries, and performance optimization strategies.

Complex Aggregation Pipelines

User Analytics Dashboard

// Get comprehensive user statistics
async function getUserAnalytics() {
  const analytics = await User.query()
    .aggregate([
      // Match active users only
      { $match: { status: 'active' } },
      
      // Add computed fields
      {
        $addFields: {
          ageGroup: {
            $switch: {
              branches: [
                { case: { $lt: ['$age', 18] }, then: 'minor' },
                { case: { $lt: ['$age', 30] }, then: 'young_adult' },
                { case: { $lt: ['$age', 50] }, then: 'adult' },
                { case: { $gte: ['$age', 50] }, then: 'senior' }
              ],
              default: 'unknown'
            }
          },
          accountAge: {
            $divide: [
              { $subtract: [new Date(), '$createdAt'] },
              1000 * 60 * 60 * 24 // Convert to days
            ]
          }
        }
      },
      
      // Group by role and age group
      {
        $group: {
          _id: {
            role: '$role',
            ageGroup: '$ageGroup'
          },
          count: { $sum: 1 },
          avgAge: { $avg: '$age' },
          avgAccountAge: { $avg: '$accountAge' },
          oldestUser: { $max: '$age' },
          newestUser: { $min: '$accountAge' }
        }
      },
      
      // Sort by count descending
      { $sort: { count: -1 } },
      
      // Group again to create final structure
      {
        $group: {
          _id: null,
          totalUsers: { $sum: '$count' },
          demographics: {
            $push: {
              role: '$_id.role',
              ageGroup: '$_id.ageGroup',
              count: '$count',
              avgAge: '$avgAge',
              avgAccountAge: '$avgAccountAge',
              oldestUser: '$oldestUser',
              newestUser: '$newestUser'
            }
          }
        }
      }
    ])

  return analytics[0] || { totalUsers: 0, demographics: [] }
}

// Usage
const analytics = await getUserAnalytics()
console.log('Total active users:', analytics.totalUsers)
console.log('Demographics:', analytics.demographics)

Content Performance Analysis

// Analyze post performance with author information
async function getContentAnalytics(timeframe: { start: Date, end: Date }) {
  const contentStats = await Post.query()
    .aggregate([
      // Match posts in timeframe
      {
        $match: {
          createdAt: {
            $gte: timeframe.start,
            $lte: timeframe.end
          },
          isPublished: true
        }
      },
      
      // Lookup author information
      {
        $lookup: {
          from: 'users',
          localField: 'authorId',
          foreignField: '_id',
          as: 'author'
        }
      },
      
      // Unwind author array
      { $unwind: '$author' },
      
      // Add computed fields
      {
        $addFields: {
          engagementScore: {
            $multiply: [
              { $add: ['$viewCount', 1] },
              { $size: { $ifNull: ['$tags', []] } }
            ]
          },
          contentLength: { $strLenCP: '$content' },
          titleLength: { $strLenCP: '$title' }
        }
      },
      
      // Group by author
      {
        $group: {
          _id: '$authorId',
          authorName: { $first: '$author.name' },
          authorEmail: { $first: '$author.email' },
          postCount: { $sum: 1 },
          totalViews: { $sum: '$viewCount' },
          avgViews: { $avg: '$viewCount' },
          totalEngagement: { $sum: '$engagementScore' },
          avgContentLength: { $avg: '$contentLength' },
          avgTitleLength: { $avg: '$titleLength' },
          topPost: {
            $max: {
              title: '$title',
              views: '$viewCount',
              engagement: '$engagementScore'
            }
          },
          tags: { $push: '$tags' }
        }
      },
      
      // Flatten and count unique tags
      {
        $addFields: {
          uniqueTags: {
            $size: {
              $setUnion: {
                $reduce: {
                  input: '$tags',
                  initialValue: [],
                  in: { $setUnion: ['$$value', '$$this'] }
                }
              }
            }
          }
        }
      },
      
      // Sort by total engagement
      { $sort: { totalEngagement: -1 } },
      
      // Limit to top 20 authors
      { $limit: 20 }
    ])

  return contentStats
}

// Usage
const contentAnalytics = await getContentAnalytics({
  start: new Date('2024-01-01'),
  end: new Date('2024-12-31')
})

contentAnalytics.forEach(author => {
  console.log(`${author.authorName}: ${author.postCount} posts, ${author.totalViews} views`)
})

Multi-Field Search with Scoring

// Advanced search with custom scoring
async function advancedSearch(searchQuery: string, options: {
  includeUnpublished?: boolean
  authorFilter?: string
  tagFilter?: string[]
  minViews?: number
  sortBy?: 'relevance' | 'date' | 'popularity'
  page?: number
  perPage?: number
}) {
  const pipeline = []

  // Text search stage
  if (searchQuery) {
    pipeline.push({
      $match: {
        $text: {
          $search: searchQuery,
          $caseSensitive: false,
          $diacriticSensitive: false
        }
      }
    })
  }

  // Add text score
  pipeline.push({
    $addFields: {
      textScore: { $meta: 'textScore' },
      // Custom relevance scoring
      relevanceScore: {
        $add: [
          { $meta: 'textScore' },
          { $multiply: [{ $ln: { $add: ['$viewCount', 1] } }, 0.1] },
          { $cond: [{ $eq: ['$isPublished', true] }, 0.5, 0] }
        ]
      }
    }
  })

  // Apply filters
  const matchConditions: any = {}

  if (!options.includeUnpublished) {
    matchConditions.isPublished = true
  }

  if (options.minViews) {
    matchConditions.viewCount = { $gte: options.minViews }
  }

  if (options.tagFilter && options.tagFilter.length > 0) {
    matchConditions.tags = { $in: options.tagFilter }
  }

  if (Object.keys(matchConditions).length > 0) {
    pipeline.push({ $match: matchConditions })
  }

  // Lookup author if needed
  if (options.authorFilter) {
    pipeline.push(
      {
        $lookup: {
          from: 'users',
          localField: 'authorId',
          foreignField: '_id',
          as: 'author'
        }
      },
      { $unwind: '$author' },
      {
        $match: {
          'author.name': { $regex: options.authorFilter, $options: 'i' }
        }
      }
    )
  }

  // Sorting
  let sortStage: any
  switch (options.sortBy) {
    case 'relevance':
      sortStage = { relevanceScore: -1, createdAt: -1 }
      break
    case 'date':
      sortStage = { createdAt: -1 }
      break
    case 'popularity':
      sortStage = { viewCount: -1, createdAt: -1 }
      break
    default:
      sortStage = { relevanceScore: -1, createdAt: -1 }
  }

  pipeline.push({ $sort: sortStage })

  // Pagination
  const page = options.page || 1
  const perPage = options.perPage || 20
  const skip = (page - 1) * perPage

  // Add pagination and count
  pipeline.push({
    $facet: {
      data: [
        { $skip: skip },
        { $limit: perPage },
        {
          $project: {
            title: 1,
            content: { $substr: ['$content', 0, 200] }, // Excerpt
            slug: 1,
            authorId: 1,
            tags: 1,
            viewCount: 1,
            createdAt: 1,
            textScore: 1,
            relevanceScore: 1,
            author: { $ifNull: ['$author', null] }
          }
        }
      ],
      totalCount: [{ $count: 'count' }]
    }
  })

  const results = await Post.query().aggregate(pipeline)
  const result = results[0]

  return {
    data: result.data,
    meta: {
      total: result.totalCount[0]?.count || 0,
      page,
      perPage,
      lastPage: Math.ceil((result.totalCount[0]?.count || 0) / perPage)
    }
  }
}

// Usage
const searchResults = await advancedSearch('mongodb tutorial', {
  includeUnpublished: false,
  tagFilter: ['database', 'tutorial'],
  minViews: 100,
  sortBy: 'relevance',
  page: 1,
  perPage: 10
})

Geospatial Queries

// Location model for geospatial examples
class Location extends BaseModel {
  @column({ isPrimary: true })
  declare _id: string

  @column()
  declare name: string

  @column()
  declare type: 'restaurant' | 'hotel' | 'attraction' | 'store'

  @column()
  declare coordinates: {
    type: 'Point'
    coordinates: [number, number] // [longitude, latitude]
  }

  @column()
  declare address: string

  @column()
  declare rating: number

  @column()
  declare priceRange: '$' | '$$' | '$$$' | '$$$$'

  static get indexes() {
    return [
      { coordinates: '2dsphere' }, // Geospatial index
      { type: 1, rating: -1 }
    ]
  }
}

// Find nearby locations
async function findNearbyLocations(
  userLocation: { lat: number, lng: number },
  options: {
    maxDistance?: number // in meters
    type?: string
    minRating?: number
    priceRange?: string[]
    limit?: number
  } = {}
) {
  const pipeline = [
    // Geospatial search
    {
      $geoNear: {
        near: {
          type: 'Point',
          coordinates: [userLocation.lng, userLocation.lat]
        },
        distanceField: 'distance',
        maxDistance: options.maxDistance || 5000, // 5km default
        spherical: true,
        distanceMultiplier: 1 // Distance in meters
      }
    }
  ]

  // Apply filters
  const matchConditions: any = {}

  if (options.type) {
    matchConditions.type = options.type
  }

  if (options.minRating) {
    matchConditions.rating = { $gte: options.minRating }
  }

  if (options.priceRange && options.priceRange.length > 0) {
    matchConditions.priceRange = { $in: options.priceRange }
  }

  if (Object.keys(matchConditions).length > 0) {
    pipeline.push({ $match: matchConditions })
  }

  // Sort by distance and rating
  pipeline.push({
    $sort: {
      distance: 1,
      rating: -1
    }
  })

  // Limit results
  if (options.limit) {
    pipeline.push({ $limit: options.limit })
  }

  // Format distance for display
  pipeline.push({
    $addFields: {
      distanceKm: { $round: [{ $divide: ['$distance', 1000] }, 2] },
      distanceMiles: { $round: [{ $divide: ['$distance', 1609.34] }, 2] }
    }
  })

  return await Location.query().aggregate(pipeline)
}

// Find locations within a polygon (e.g., city boundaries)
async function findLocationsInArea(polygon: number[][]) {
  return await Location.query()
    .where('coordinates', 'geoWithin', {
      $geometry: {
        type: 'Polygon',
        coordinates: [polygon]
      }
    })
    .orderBy('rating', 'desc')
    .all()
}

// Usage
const nearbyRestaurants = await findNearbyLocations(
  { lat: 40.7589, lng: -73.9851 }, // Times Square, NYC
  {
    maxDistance: 2000, // 2km
    type: 'restaurant',
    minRating: 4.0,
    priceRange: ['$$', '$$$'],
    limit: 20
  }
)

console.log('Found restaurants:', nearbyRestaurants.length)
nearbyRestaurants.forEach(restaurant => {
  console.log(`${restaurant.name} - ${restaurant.distanceKm}km away, ${restaurant.rating}`)
})

Performance Optimization

Efficient Pagination with Cursor

// Cursor-based pagination for large datasets
async function getCursorPaginatedPosts(
  cursor?: string,
  limit: number = 20,
  filters: {
    authorId?: string
    tags?: string[]
    published?: boolean
  } = {}
) {
  const query = Post.query()

  // Apply filters
  if (filters.published !== undefined) {
    query.where('isPublished', filters.published)
  }

  if (filters.authorId) {
    query.where('authorId', filters.authorId)
  }

  if (filters.tags && filters.tags.length > 0) {
    query.whereArrayContainsAny('tags', filters.tags)
  }

  // Cursor pagination
  if (cursor) {
    query.where('_id', '<', cursor)
  }

  // Order and limit
  query
    .orderBy('_id', 'desc')
    .limit(limit + 1) // Get one extra to check if there's a next page

  const posts = await query.all()
  const hasNext = posts.length > limit

  if (hasNext) {
    posts.pop() // Remove the extra item
  }

  return {
    data: posts,
    hasNext,
    nextCursor: hasNext ? posts[posts.length - 1]._id : null
  }
}

// Optimized search with indexes
async function optimizedPostSearch(searchTerm: string, options: {
  useIndex?: boolean
  fields?: string[]
  limit?: number
}) {
  if (options.useIndex) {
    // Use text index for better performance
    return await Post.query()
      .whereText(searchTerm)
      .select(options.fields || ['title', 'slug', 'createdAt'])
      .orderBy('score', { $meta: 'textScore' })
      .limit(options.limit || 50)
      .all()
  } else {
    // Fallback to regex search
    return await Post.query()
      .where((query) => {
        query
          .where('title', 'regex', new RegExp(searchTerm, 'i'))
          .orWhere('content', 'regex', new RegExp(searchTerm, 'i'))
      })
      .select(options.fields || ['title', 'slug', 'createdAt'])
      .limit(options.limit || 50)
      .all()
  }
}

Batch Operations

// Efficient batch processing
async function batchUpdatePostViews(viewUpdates: { postId: string, views: number }[]) {
  // Group updates by post ID to avoid duplicates
  const updateMap = new Map<string, number>()
  
  viewUpdates.forEach(update => {
    const current = updateMap.get(update.postId) || 0
    updateMap.set(update.postId, current + update.views)
  })

  // Perform batch updates
  const bulkOps = Array.from(updateMap.entries()).map(([postId, viewIncrement]) => ({
    updateOne: {
      filter: { _id: postId },
      update: { $inc: { viewCount: viewIncrement } }
    }
  }))

  if (bulkOps.length > 0) {
    const collection = Post.query().collection()
    const result = await collection.bulkWrite(bulkOps)
    
    console.log(`Updated ${result.modifiedCount} posts`)
    return result
  }
}

// Batch create with validation
async function batchCreateUsers(usersData: any[], batchSize: number = 100) {
  const results = []
  
  // Process in batches to avoid memory issues
  for (let i = 0; i < usersData.length; i += batchSize) {
    const batch = usersData.slice(i, i + batchSize)
    
    try {
      const batchResults = await User.createMany(batch)
      results.push(...batchResults)
      
      console.log(`Processed batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(usersData.length / batchSize)}`)
    } catch (error) {
      console.error(`Batch ${Math.floor(i / batchSize) + 1} failed:`, error.message)
      
      // Process individually to identify problematic records
      for (const userData of batch) {
        try {
          const user = await User.create(userData)
          results.push(user)
        } catch (individualError) {
          console.error(`Failed to create user ${userData.email}:`, individualError.message)
        }
      }
    }
  }
  
  return results
}

Real-Time Analytics

Live Dashboard Queries

// Real-time metrics for dashboard
async function getDashboardMetrics() {
  const now = new Date()
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  const thisWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
  const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1)

  const [
    totalUsers,
    activeUsers,
    newUsersToday,
    newUsersThisWeek,
    totalPosts,
    publishedPosts,
    postsToday,
    topAuthors,
    popularTags
  ] = await Promise.all([
    // Total users
    User.query().count(),
    
    // Active users (logged in within 30 days)
    User.query()
      .where('lastLoginAt', '>=', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
      .count(),
    
    // New users today
    User.query()
      .where('createdAt', '>=', today)
      .count(),
    
    // New users this week
    User.query()
      .where('createdAt', '>=', thisWeek)
      .count(),
    
    // Total posts
    Post.query().count(),
    
    // Published posts
    Post.query()
      .where('isPublished', true)
      .count(),
    
    // Posts created today
    Post.query()
      .where('createdAt', '>=', today)
      .count(),
    
    // Top authors by post count
    Post.query()
      .aggregate([
        { $match: { isPublished: true } },
        {
          $group: {
            _id: '$authorId',
            postCount: { $sum: 1 },
            totalViews: { $sum: '$viewCount' }
          }
        },
        {
          $lookup: {
            from: 'users',
            localField: '_id',
            foreignField: '_id',
            as: 'author'
          }
        },
        { $unwind: '$author' },
        {
          $project: {
            authorName: '$author.name',
            postCount: 1,
            totalViews: 1
          }
        },
        { $sort: { postCount: -1 } },
        { $limit: 5 }
      ]),
    
    // Popular tags
    Post.query()
      .aggregate([
        { $match: { isPublished: true } },
        { $unwind: '$tags' },
        {
          $group: {
            _id: '$tags',
            count: { $sum: 1 },
            totalViews: { $sum: '$viewCount' }
          }
        },
        { $sort: { count: -1 } },
        { $limit: 10 }
      ])
  ])

  return {
    users: {
      total: totalUsers,
      active: activeUsers,
      newToday: newUsersToday,
      newThisWeek: newUsersThisWeek
    },
    posts: {
      total: totalPosts,
      published: publishedPosts,
      createdToday: postsToday
    },
    topAuthors,
    popularTags
  }
}

// Usage
const metrics = await getDashboardMetrics()
console.log('Dashboard metrics:', metrics)

Next Steps