Implementing Search and Filtering

1. Reading Search Query

Example: Search param

app.get("/products", (req, res) => {
  const q = (req.query.search || "").trim();
  // ... build query
});

Example: ILIKE (Postgres)

SELECT * FROM products WHERE name ILIKE '%' || $1 || '%' LIMIT 50;

Example: Prisma contains

const items = await prisma.product.findMany({
  where: { name: { contains: q, mode: "insensitive" } },
  take: 50
});

3. Using Multiple Filters

Example: AND/OR with Prisma

const where = {
  AND: [
    req.query.category && { category: req.query.category },
    req.query.minPrice && { price: { gte: Number(req.query.minPrice) } },
    req.query.maxPrice && { price: { lte: Number(req.query.maxPrice) } }
  ].filter(Boolean)
};
LogicPattern
ANDDefault — all conditions must match
ORWrap in OR: [...]
NOTNOT: { ... }

4. Filtering by Date Range

Example: from/to

const { from, to } = req.query;
const where = {
  createdAt: {
    ...(from && { gte: new Date(from) }),
    ...(to   && { lte: new Date(to) })
  }
};

Example: Postgres trigram

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX products_name_trgm ON products USING GIN (name gin_trgm_ops);

SELECT *, similarity(name, $1) AS score
FROM products
WHERE name % $1
ORDER BY score DESC
LIMIT 20;
EngineTool
Postgrespg_trgm, tsvector
MongoDBAtlas Search, $text
ExternalOpenSearch, Meilisearch, Typesense

6. Creating Complex Query Builders

Example: Reusable builder

function buildProductFilter(q) {
  const where = {};
  if (q.search)   where.OR = [{ name: { contains: q.search } }, { sku: q.search }];
  if (q.category) where.category = q.category;
  if (q.inStock === "true") where.stock = { gt: 0 };
  if (q.minPrice || q.maxPrice) {
    where.price = {};
    if (q.minPrice) where.price.gte = Number(q.minPrice);
    if (q.maxPrice) where.price.lte = Number(q.maxPrice);
  }
  return where;
}

7. Validating Filter Parameters

Example: zod filter schema

const filterSchema = z.object({
  search:   z.string().max(100).optional(),
  category: z.enum(["books","music","games"]).optional(),
  minPrice: z.coerce.number().min(0).optional(),
  maxPrice: z.coerce.number().min(0).optional(),
  inStock:  z.enum(["true","false"]).optional()
}).refine(d => !d.minPrice || !d.maxPrice || d.minPrice <= d.maxPrice,
          { message: "minPrice must be ≤ maxPrice" });

8. Using Query Operators

OperatorMeaningExample
gt / gte> / ≥?price[gt]=100
lt / lte< / ≤?price[lte]=500
inIN list?status[in]=open,pending
ne?status[ne]=archived
likeLIKE?name[like]=foo%25

Example: Postgres tsvector

ALTER TABLE articles ADD COLUMN search tsvector
  GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX articles_search_idx ON articles USING GIN (search);

SELECT *, ts_rank(search, q) AS rank
FROM articles, plainto_tsquery('english', $1) q
WHERE search @@ q
ORDER BY rank DESC LIMIT 20;

10. Sanitizing Search Input

Example: Strip dangerous chars

function sanitizeSearch(s) {
  return String(s)
    .slice(0, 100)                  // length cap
    .replace(/[%_\\]/g, "\\
function sanitizeSearch(s) {
  return String(s)
    .slice(0, 100)                  // length cap
    .replace(/[%_\\]/g, "\\$&");   // escape SQL LIKE wildcards
}
#x26;"
); // escape SQL LIKE wildcards
}
Warning: Always use parameterized queries — never interpolate user input into SQL even after "sanitization".