Filtering and Sorting (ফিল্টারিং ও সর্টিং)
Filtering ও Sorting কী?
Filtering হলো data-এর মধ্য থেকে শর্ত অনুযায়ী নির্দিষ্ট items বের করা। Sorting হলো data-কে নির্দিষ্ট ক্রমে সাজানো।
Database: 100,000 products
Filtering:
GET /api/products?category=electronics&price_max=50000
→ শুধু electronics category ও 50,000 টাকার নিচের products
Sorting:
GET /api/products?sort=price&order=asc
→ দাম কম থেকে বেশি ক্রমে সাজাও
Filter + Sort:
GET /api/products?category=electronics&price_max=50000&sort=price&order=asc
→ Electronics, 50K-এর নিচে, দাম অনুযায়ী ascendingবাস্তব উদাহরণ: Daraz/Amazon-এ "Electronics" category select করো → price range দাও → "Price: Low to High" sort করো। Backend-এ ঠিক এটাই ঘটছে।
URL Query Parameter Design
Common Patterns
Exact Match: ?status=active
?role=admin
?category=electronics
Comparison: ?price_min=100&price_max=5000
?age_gte=18&age_lte=65
?created_after=2026-01-01
Search (Partial): ?search=ripon
?q=macbook+pro
Multiple Values: ?status=active,pending
?category=electronics,books
?tags=javascript,nodejs
Boolean: ?is_active=true
?featured=true
?in_stock=true
Sorting: ?sort=price&order=asc
?sort=-price (minus = descending)
?sort=price:asc,name:desc
Pagination: ?page=2&limit=20
Field Selection: ?fields=name,email,createdAtURL Design Best Practices
✅ Good:
/api/products?category=electronics&price_min=100&sort=price&order=asc
❌ Bad:
/api/products/category/electronics/price/100-5000/sort/price/asc
(URL path-এ filter দেওয়া — messy, inflexible)
✅ Naming Convention:
snake_case → price_min, created_at, is_active
বা camelCase → priceMin, createdAt, isActive
(project-এ consistent থাকো)Filtering Implementation
Basic Filtering (Express + MongoDB)
app.get("/api/products", async (req, res) => {
const filter = {};
// Exact match
if (req.query.category) {
filter.category = req.query.category;
}
if (req.query.status) {
filter.status = req.query.status;
}
if (req.query.brand) {
filter.brand = req.query.brand;
}
// Boolean
if (req.query.in_stock !== undefined) {
filter.inStock = req.query.in_stock === "true";
}
if (req.query.featured !== undefined) {
filter.featured = req.query.featured === "true";
}
// Range (price)
if (req.query.price_min || req.query.price_max) {
filter.price = {};
if (req.query.price_min)
filter.price.$gte = parseFloat(req.query.price_min);
if (req.query.price_max)
filter.price.$lte = parseFloat(req.query.price_max);
}
// Date range
if (req.query.from || req.query.to) {
filter.createdAt = {};
if (req.query.from) filter.createdAt.$gte = new Date(req.query.from);
if (req.query.to) filter.createdAt.$lte = new Date(req.query.to);
}
// Multiple values (comma-separated)
if (req.query.tags) {
filter.tags = { $in: req.query.tags.split(",") };
}
const products = await Product.find(filter).limit(20);
res.json({ data: products });
});Text Search
// Simple regex search (ছোট dataset-এ)
if (req.query.search) {
filter.$or = [
{ name: { $regex: req.query.search, $options: "i" } },
{ description: { $regex: req.query.search, $options: "i" } },
];
}
// MongoDB Full-Text Search (বড় dataset-এ)
// Schema: productSchema.index({ name: 'text', description: 'text' });
if (req.query.q) {
filter.$text = { $search: req.query.q };
}Regex Search:
✅ Partial match ("mac" → "Macbook Pro")
✅ Case-insensitive
❌ Large dataset-এ slow (no index)
❌ Fuzzy search নেই
Full-Text Search:
✅ Fast (text index ব্যবহার করে)
✅ Relevance score
❌ Partial match নেই ("mac" ≠ "macbook")
❌ text index তৈরি করতে হয়
Advanced Search → Elasticsearch/Meilisearch (আলাদা search engine)Nested Object Filtering
// Address city দিয়ে filter
if (req.query.city) {
filter["address.city"] = req.query.city;
}
// Rating range
if (req.query.min_rating) {
filter["rating.average"] = { $gte: parseFloat(req.query.min_rating) };
}Sorting Implementation
Basic Sorting
app.get("/api/products", async (req, res) => {
// Allowed sort fields (whitelist — security!)
const ALLOWED_SORT_FIELDS = ["name", "price", "createdAt", "rating", "sales"];
let sort = { createdAt: -1 }; // default: newest first
if (req.query.sort) {
const sortField = req.query.sort;
const sortOrder = req.query.order === "asc" ? 1 : -1;
if (ALLOWED_SORT_FIELDS.includes(sortField)) {
sort = { [sortField]: sortOrder };
}
}
const products = await Product.find(filter).sort(sort).limit(20);
res.json({ data: products });
});Alternative Sort Syntax
Style 1: Separate params
?sort=price&order=asc
Style 2: Prefix (- for descending)
?sort=-price → descending
?sort=price → ascending
Style 3: Colon syntax
?sort=price:asc
?sort=price:desc
Style 4: Multiple sort
?sort=price:asc,name:desc
?sort=-price,nameMulti-Field Sorting
function parseSort(sortString, allowedFields) {
if (!sortString) return { createdAt: -1 };
const sort = {};
const fields = sortString.split(",");
for (const field of fields) {
// Style: "-price" or "price"
if (field.startsWith("-")) {
const name = field.slice(1);
if (allowedFields.includes(name)) sort[name] = -1;
} else {
if (allowedFields.includes(field)) sort[field] = 1;
}
}
return Object.keys(sort).length > 0 ? sort : { createdAt: -1 };
}
// Usage:
// ?sort=-price,name → { price: -1, name: 1 }
// ?sort=rating → { rating: 1 }
const sort = parseSort(req.query.sort, ALLOWED_SORT_FIELDS);Colon-Style Multi-Sort
function parseSortColon(sortString, allowedFields) {
if (!sortString) return { createdAt: -1 };
const sort = {};
const fields = sortString.split(",");
for (const field of fields) {
const [name, order] = field.split(":");
if (allowedFields.includes(name)) {
sort[name] = order === "desc" ? -1 : 1;
}
}
return Object.keys(sort).length > 0 ? sort : { createdAt: -1 };
}
// ?sort=price:asc,name:desc → { price: 1, name: -1 }Sorting Security
// ❌ User-এর input সরাসরি sort-এ ব্যবহার করো না!
const sort = { [req.query.sort]: req.query.order };
// Attack: ?sort=password&order=1 → password field দেখে ফেলবে!
// ✅ Whitelist approach
const ALLOWED = ["name", "price", "createdAt"];
if (!ALLOWED.includes(req.query.sort)) {
return res.status(400).json({ error: "Invalid sort field" });
}Field Selection (Sparse Fieldsets)
Client-কে শুধু দরকারি fields আনতে দাও:
GET /api/products?fields=name,price,image
→ শুধু name, price, image return করো (bandwidth save)function parseFields(fieldsString, allowedFields) {
if (!fieldsString) return null;
const requested = fieldsString.split(",");
const valid = requested.filter((f) => allowedFields.includes(f));
return valid.length > 0 ? valid.join(" ") : null;
}
const ALLOWED_FIELDS = [
"name",
"price",
"description",
"image",
"category",
"rating",
"createdAt",
];
app.get("/api/products", async (req, res) => {
const fields = parseFields(req.query.fields, ALLOWED_FIELDS);
let query = Product.find(filter).sort(sort);
if (fields) query = query.select(fields);
const products = await query.limit(20);
res.json({ data: products });
});SQL Implementation
Filtering
SELECT id, name, price, category
FROM products
WHERE category = 'electronics'
AND price BETWEEN 100 AND 5000
AND in_stock = true
AND name ILIKE '%macbook%'
ORDER BY price ASC
LIMIT 20 OFFSET 0;Parameterized Query Builder
function buildQuery(filters) {
const conditions = [];
const params = [];
let paramIndex = 1;
if (filters.category) {
conditions.push(`category = $${paramIndex++}`);
params.push(filters.category);
}
if (filters.price_min) {
conditions.push(`price >= $${paramIndex++}`);
params.push(parseFloat(filters.price_min));
}
if (filters.price_max) {
conditions.push(`price <= $${paramIndex++}`);
params.push(parseFloat(filters.price_max));
}
if (filters.search) {
conditions.push(
`(name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`,
);
params.push(`%${filters.search}%`);
paramIndex++;
}
if (filters.in_stock !== undefined) {
conditions.push(`in_stock = $${paramIndex++}`);
params.push(filters.in_stock === "true");
}
const where =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
return { where, params };
}
// Usage
const { where, params } = buildQuery(req.query);
const sql = `SELECT * FROM products ${where} ORDER BY created_at DESC LIMIT 20`;
const result = await db.query(sql, params);Prisma Implementation
app.get("/api/products", async (req, res) => {
const where = {};
if (req.query.category) where.category = req.query.category;
if (req.query.in_stock) where.inStock = req.query.in_stock === "true";
if (req.query.price_min || req.query.price_max) {
where.price = {};
if (req.query.price_min) where.price.gte = parseFloat(req.query.price_min);
if (req.query.price_max) where.price.lte = parseFloat(req.query.price_max);
}
if (req.query.search) {
where.OR = [
{ name: { contains: req.query.search, mode: "insensitive" } },
{ description: { contains: req.query.search, mode: "insensitive" } },
];
}
if (req.query.tags) {
where.tags = { hasSome: req.query.tags.split(",") };
}
const sortField = ["name", "price", "createdAt"].includes(req.query.sort)
? req.query.sort
: "createdAt";
const sortOrder = req.query.order === "asc" ? "asc" : "desc";
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
orderBy: { [sortField]: sortOrder },
take: 20,
skip: 0,
}),
prisma.product.count({ where }),
]);
res.json({ data: products, total });
});Reusable Filter Builder
প্রতিটি route-এ filter logic repeat না করে reusable builder তৈরি করো:
class QueryBuilder {
constructor(model) {
this.model = model;
this.filter = {};
this.sortObj = { createdAt: -1 };
this.projection = null;
this.skipVal = 0;
this.limitVal = 20;
}
exact(field, value) {
if (value !== undefined && value !== null && value !== "") {
this.filter[field] = value;
}
return this;
}
boolean(field, value) {
if (value !== undefined) {
this.filter[field] = value === "true";
}
return this;
}
range(field, min, max) {
if (min || max) {
this.filter[field] = {};
if (min) this.filter[field].$gte = parseFloat(min);
if (max) this.filter[field].$lte = parseFloat(max);
}
return this;
}
dateRange(field, from, to) {
if (from || to) {
this.filter[field] = {};
if (from) this.filter[field].$gte = new Date(from);
if (to) this.filter[field].$lte = new Date(to);
}
return this;
}
search(fields, term) {
if (term) {
this.filter.$or = fields.map((f) => ({
[f]: { $regex: term, $options: "i" },
}));
}
return this;
}
inArray(field, values) {
if (values) {
const arr = typeof values === "string" ? values.split(",") : values;
this.filter[field] = { $in: arr };
}
return this;
}
sort(sortString, allowedFields) {
if (sortString) {
this.sortObj = parseSort(sortString, allowedFields);
}
return this;
}
select(fieldsString, allowedFields) {
if (fieldsString) {
const fields = fieldsString
.split(",")
.filter((f) => allowedFields.includes(f));
if (fields.length) this.projection = fields.join(" ");
}
return this;
}
paginate(page, limit, maxLimit = 100) {
this.limitVal = Math.min(maxLimit, Math.max(1, parseInt(limit) || 20));
const p = Math.max(1, parseInt(page) || 1);
this.skipVal = (p - 1) * this.limitVal;
return this;
}
async exec() {
let query = this.model.find(this.filter);
if (this.projection) query = query.select(this.projection);
query = query.sort(this.sortObj).skip(this.skipVal).limit(this.limitVal);
const [data, total] = await Promise.all([
query.lean(),
this.model.countDocuments(this.filter),
]);
const totalPages = Math.ceil(total / this.limitVal);
const page = Math.floor(this.skipVal / this.limitVal) + 1;
return {
data,
pagination: {
page,
limit: this.limitVal,
total,
totalPages,
hasNextPage: page < totalPages,
hasPrevPage: page > 1,
},
};
}
}QueryBuilder ব্যবহার
const SORT_FIELDS = ["name", "price", "createdAt", "rating"];
const SELECT_FIELDS = [
"name",
"price",
"image",
"category",
"rating",
"createdAt",
];
app.get("/api/products", async (req, res) => {
const result = await new QueryBuilder(Product)
.exact("category", req.query.category)
.exact("brand", req.query.brand)
.boolean("inStock", req.query.in_stock)
.boolean("featured", req.query.featured)
.range("price", req.query.price_min, req.query.price_max)
.range("rating.average", req.query.min_rating, req.query.max_rating)
.dateRange("createdAt", req.query.from, req.query.to)
.search(["name", "description"], req.query.search)
.inArray("tags", req.query.tags)
.sort(req.query.sort, SORT_FIELDS)
.select(req.query.fields, SELECT_FIELDS)
.paginate(req.query.page, req.query.limit)
.exec();
res.json(result);
});
// GET /api/products?category=electronics&price_min=100&price_max=5000
// &search=macbook&sort=-price&fields=name,price,image&page=1&limit=10Python — FastAPI
from fastapi import FastAPI, Query
from typing import Optional
app = FastAPI()
@app.get("/api/products")
async def get_products(
category: Optional[str] = None,
brand: Optional[str] = None,
in_stock: Optional[bool] = None,
price_min: Optional[float] = Query(None, ge=0),
price_max: Optional[float] = Query(None, ge=0),
search: Optional[str] = Query(None, max_length=200),
tags: Optional[str] = None,
sort: str = Query("created_at", regex="^(name|price|created_at|rating)$"),
order: str = Query("desc", regex="^(asc|desc)$"),
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100)
):
query = {}
if category:
query["category"] = category
if brand:
query["brand"] = brand
if in_stock is not None:
query["in_stock"] = in_stock
if price_min is not None or price_max is not None:
query["price"] = {}
if price_min is not None:
query["price"]["$gte"] = price_min
if price_max is not None:
query["price"]["$lte"] = price_max
if search:
query["$or"] = [
{"name": {"$regex": search, "$options": "i"}},
{"description": {"$regex": search, "$options": "i"}}
]
if tags:
query["tags"] = {"$in": tags.split(",")}
skip = (page - 1) * limit
sort_direction = 1 if order == "asc" else -1
products = await db.products.find(query)\
.sort(sort, sort_direction)\
.skip(skip)\
.limit(limit)\
.to_list(limit)
total = await db.products.count_documents(query)
return {
"data": products,
"pagination": {
"page": page,
"limit": limit,
"total": total,
"total_pages": -(-total // limit)
}
}Advanced Filtering Patterns
1. Faceted Search (Count Per Filter)
E-commerce-এ common — প্রতিটি filter value-তে কতগুলো item আছে দেখানো:
// "Electronics (234), Books (89), Clothing (156)"
app.get("/api/products/facets", async (req, res) => {
const facets = await Product.aggregate([
{ $match: filter }, // current filters apply করো
{
$facet: {
categories: [
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
],
brands: [
{ $group: { _id: "$brand", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
],
priceRanges: [
{
$bucket: {
groupBy: "$price",
boundaries: [0, 1000, 5000, 10000, 50000, Infinity],
default: "Other",
output: { count: { $sum: 1 } },
},
},
],
ratings: [
{
$group: { _id: { $floor: "$rating.average" }, count: { $sum: 1 } },
},
{ $sort: { _id: -1 } },
],
},
},
]);
res.json(facets[0]);
});{
"categories": [
{ "_id": "electronics", "count": 234 },
{ "_id": "clothing", "count": 156 },
{ "_id": "books", "count": 89 }
],
"brands": [
{ "_id": "Apple", "count": 45 },
{ "_id": "Samsung", "count": 38 }
],
"priceRanges": [
{ "_id": 0, "count": 120 },
{ "_id": 1000, "count": 340 },
{ "_id": 5000, "count": 200 }
]
}2. Geo-Location Filtering
// কাছের restaurants খুঁজো
// Schema: locationSchema.index({ location: '2dsphere' });
if (req.query.lat && req.query.lng) {
const maxDistance = parseInt(req.query.radius) || 5000; // 5km default
filter.location = {
$near: {
$geometry: {
type: "Point",
coordinates: [parseFloat(req.query.lng), parseFloat(req.query.lat)],
},
$maxDistance: maxDistance,
},
};
}
// GET /api/restaurants?lat=23.8103&lng=90.4125&radius=30003. Aggregation Pipeline Filtering
// Complex filtering with computed fields
const pipeline = [
{ $match: filter },
{
$addFields: {
discountedPrice: {
$multiply: [
"$price",
{ $subtract: [1, { $divide: ["$discount", 100] }] },
],
},
},
},
{ $match: { discountedPrice: { $lte: 5000 } } },
{ $sort: { discountedPrice: 1 } },
{ $skip: skip },
{ $limit: limit },
];
const products = await Product.aggregate(pipeline);Database Indexes
Filtering ও sorting fast করার key হলো proper indexes:
// MongoDB indexes
productSchema.index({ category: 1 }); // Exact match filter
productSchema.index({ price: 1 }); // Range filter
productSchema.index({ category: 1, price: 1 }); // Compound filter
productSchema.index({ name: "text", description: "text" }); // Full-text search
productSchema.index({ createdAt: -1 }); // Sort
productSchema.index({ category: 1, price: 1, createdAt: -1 }); // Filter + Sort-- SQL indexes
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_cat_price ON products(category, price);
CREATE INDEX idx_products_created ON products(created_at DESC);Index Strategy:
─────────────────────────────────────
1. সবচেয়ে বেশি filter হয় এমন field-এ index দাও
2. Compound index-এ order matters: equality → range → sort
{ category: 1, price: 1, createdAt: -1 }
✅ category = 'electronics' AND price < 5000 ORDER BY createdAt
3. প্রতিটি query-তে আলাদা index দরকার না — compound index অনেক query cover করে
4. Too many indexes → write performance কমে (insert/update slow)Filter Validation
User-এর filter input-ও validate করো:
const Joi = require("joi");
const productFilterSchema = Joi.object({
category: Joi.string()
.valid("electronics", "books", "clothing", "food")
.optional(),
brand: Joi.string().max(50).optional(),
in_stock: Joi.string().valid("true", "false").optional(),
featured: Joi.string().valid("true", "false").optional(),
price_min: Joi.number().min(0).optional(),
price_max: Joi.number().min(0).optional(),
min_rating: Joi.number().min(0).max(5).optional(),
search: Joi.string().max(200).trim().optional(),
tags: Joi.string().max(500).optional(),
from: Joi.date().iso().optional(),
to: Joi.date().iso().optional(),
sort: Joi.string().valid("name", "price", "createdAt", "rating").optional(),
order: Joi.string().valid("asc", "desc").optional(),
fields: Joi.string().max(200).optional(),
page: Joi.number().integer().min(1).optional(),
limit: Joi.number().integer().min(1).max(100).optional(),
}).with("price_max", "price_min"); // price_max দিলে price_min-ও দিতে হবে (optional)
app.get(
"/api/products",
validate(productFilterSchema, "query"),
async (req, res) => {
// ... validated query params
},
);সংক্ষেপে মনে রাখার সূত্র
Filtering = শর্ত অনুযায়ী data বের করা
Sorting = নির্দিষ্ট ক্রমে সাজানো
Field Selection = শুধু দরকারি fields return
Filter Types:
Exact → ?category=electronics
Range → ?price_min=100&price_max=5000
Search → ?search=macbook (regex/text index)
Boolean → ?in_stock=true
Multiple → ?tags=js,node (comma-separated)
Date → ?from=2026-01-01&to=2026-12-31
Geo → ?lat=23.81&lng=90.41&radius=5000
Sort Syntax:
?sort=price&order=asc → separate params
?sort=-price → prefix style
?sort=price:asc,name:desc → colon style + multi-sort
Security:
✅ Whitelist sort fields (NEVER use raw input)
✅ Validate filter values (Joi/Zod)
✅ Whitelist field selection
✅ Max limit enforce
Performance:
✅ Database index দাও (filter + sort fields)
✅ Compound index: equality → range → sort
✅ Projection (select) ব্যবহার করো
✅ .lean() (Mongoose)
QueryBuilder Pattern → Reusable, chainable filter builder
Faceted Search → প্রতিটি filter-এ count দেখাও (e-commerce)Interview Golden Lines
Always whitelist sort and filter fields — accepting arbitrary field names exposes internal data and enables injection attacks.
Compound database indexes should follow the ESR rule: Equality fields first, then Sort fields, then Range fields.
Faceted search counts items per filter value (like "Electronics: 234") — implemented via aggregation pipelines.
Text search with regex works for small datasets, but large-scale search needs dedicated engines like Elasticsearch or Meilisearch.
A reusable QueryBuilder pattern eliminates duplicate filter logic across endpoints — chain exact, range, search, sort, and paginate in one fluent API.
Field selection (sparse fieldsets) reduces response size and improves performance — only return what the client needs.