Advanced Search Filters

Understanding Complex Search Requirements

Imagine you're searching for a classroom in a large school. Sometimes you might want to find a classroom by its name, like "Mr. Collins' Room". Other times, you might need to find classrooms that can hold between 20 and 30 students. These different search needs require different types of filters. In our application, we need to implement three main types of searches:

First, we need pattern matching for classroom names, similar to how you might search for a contact in your phone. Second, we need range-based searches for student limits, like finding rooms that can hold between 20 and 30 students. Finally, we need exact matching for specific student limit values, like finding all rooms that hold exactly 25 students.

A Complete Solution

Let's implement these search capabilities step by step in our classroom endpoint:

// routes/api/classrooms.js
const express = require('express');
const { Op } = require('sequelize');
const { Classroom } = require('../../db/models');

router.get('/', async (req, res) => {
    try {
        // Initialize where clause for our filters
        const where = {};
        
        // Initialize error result
        const errorResult = {
            errors: [],
            count: 0,
            pageCount: 0
        };

        // Name pattern matching filter
        if (req.query.name) {
            // Using iLike for case-insensitive search
            where.name = {
                [Op.iLike]: `%${req.query.name}%`
            };
        }

        // Student limit filter
        if (req.query.studentLimit) {
            // Check if it's a range search (contains comma)
            if (req.query.studentLimit.includes(',')) {
                // Split the range into min and max
                const [min, max] = req.query.studentLimit.split(',')
                    .map(num => parseInt(num));

                // Validate the range values
                if (!Number.isInteger(min) || !Number.isInteger(max) || min > max) {
                    errorResult.errors.push({
                        message: 'Student Limit should be two numbers: min,max'
                    });
                } else {
                    // Add range filter to where clause
                    where.studentLimit = {
                        [Op.between]: [min, max]
                    };
                }
            } else {
                // Handle exact match case
                const limit = parseInt(req.query.studentLimit);
                
                if (!Number.isInteger(limit)) {
                    errorResult.errors.push({
                        message: 'Student Limit should be an integer'
                    });
                } else {
                    where.studentLimit = limit;
                }
            }
        }

        // If we encountered any errors, return error response
        if (errorResult.errors.length > 0) {
            return res.status(400).json(errorResult);
        }

        // Execute the query with our filters
        const classrooms = await Classroom.findAll({
            where,
            order: [['name', 'ASC']]
        });

        // Return the filtered results
        return res.json(classrooms);

    } catch (err) {
        console.error(err);
        return res.status(500).json({
            errors: [{ message: 'Server error occurred' }],
            count: 0,
            pageCount: 0
        });
    }
});
    

Understanding Pattern Matching in Detail

Let's explore how pattern matching works using a real-world analogy. Think of a library card catalog system. When you search for "history", you might want to find books with titles like "World History", "History of Art", or "Natural History". This is exactly how our name pattern matching works for classrooms.

In our code, when we use %name%, it works like this:

// Examples of pattern matching:
'%mr%' matches:    "Mr. Collins", "Mr. Smith", "Amsterdam Room"
'%room%' matches:  "Room 101", "Classroom A", "Emergency Room"
'mr.%' matches:    "Mr. Collins", "Mr. Smith" (but not "Amsterdam Room")
    

Range-Based Searching Explained

Think about shopping for a car within a price range. You might want to see all cars priced between $20,000 and $30,000. This is similar to how our student limit range search works. When someone searches for classrooms that can hold between 20 and 30 students, our code processes it like this:

// For a query like ?studentLimit=20,30
where.studentLimit = {
    [Op.between]: [20, 30]
};

// This will find classrooms that can hold:
// - Exactly 20 students
// - Exactly 25 students
// - Exactly 30 students
// - Any number in between
    

Error Handling and Validation

Just as a librarian would help guide you if you made an unclear request, our code needs to handle invalid search parameters gracefully. We validate inputs in several ways:

// Range validation
if (!Number.isInteger(min) || !Number.isInteger(max) || min > max) {
    // This catches:
    // - Non-numeric values: ?studentLimit=abc,def
    // - Invalid ranges: ?studentLimit=30,20
    // - Partial ranges: ?studentLimit=20,
}

// Single value validation
if (!Number.isInteger(limit)) {
    // This catches:
    // - Non-numeric values: ?studentLimit=abc
    // - Decimal values: ?studentLimit=20.5
}
    

Testing Your Implementation

When testing this feature, think about testing it like you would test a search engine. Try different types of searches:

Name Pattern Tests

Test searches for:

GET /classrooms?name=mr          // Should find all "Mr." classrooms
GET /classrooms?name=collins     // Should find "Mr. Collins" classroom
GET /classrooms?name=xyz         // Should return empty results
    

Student Limit Tests

Test both range and exact matches:

GET /classrooms?studentLimit=20,30    // Range search
GET /classrooms?studentLimit=25        // Exact match
GET /classrooms?studentLimit=abc       // Invalid input
GET /classrooms?studentLimit=30,20     // Invalid range
    

Performance Considerations

When implementing search filters, think about performance like you would think about organizing a library. Just as a library uses different organizational systems (author index, title index, genre categories) to make searching efficient, we can optimize our database searches:

Consider adding indexes for frequently searched columns:

// In your migration file:
await queryInterface.addIndex('Classrooms', ['name']);
await queryInterface.addIndex('Classrooms', ['studentLimit']);
    

Real-World Applications

These search patterns appear in many familiar applications:

Pattern Matching:

- Email search in your inbox

- Contact search on your phone

- Document search in file systems

Range Searches:

- Price ranges on shopping sites

- Date ranges in calendar apps

- Age ranges in dating apps

Exact Matching:

- Product SKU lookup

- Employee ID search

- Room number search