Building a Niacinamide Filter: How to Tag Skincare Products by Active Ingredient

Building a Niacinamide Filter: How to Tag Skincare Products by Active Ingredient

Published Jul 6, 202619 min read

Your product catalogue is a wall of raw INCI strings, and your PM just dropped a deceptively simple ask: "Can we let users filter for niacinamide?" On the surface it looks like a one-line WHERE ingredient = 'niacinamide' query against your products table. It is not. INCI declarations are messy — the same molecule shows up as Nicotinamide, Vitamin B3, or Niacinamide PC, casing is inconsistent, the active is buried mid-list, and nothing in the string tells you the concentration. Users who want skincare products with niacinamide are actively searching your catalogue for this active, and a naive string match fails two ways: it either misses products (an empty shelf, and users churn) or it over-tags (a serum with a trace of niacinamide gets flagged as a "niacinamide product," and shoppers stop trusting every tag you show them). The demand is real — market analysts at Grand View Research put the niacinamide beauty products market at roughly USD 590.2 million in 2024, projected to reach USD 846.5 million by 2030. By the end of this walkthrough you'll be able to build a synonym-aware niacinamide tagging pipeline that generalizes to any active ingredient in your catalogue.

Overhead shot of a developer's dual-monitor desk — one screen showing a JSON array of INCI ingredients, the other showing a skincare e-commerce product page with a filter sidebar. Warm, realistic office lighting, slightly angled.

Table of Contents

Why "Just Search for Niacinamide" Breaks in Production

The WHERE ingredient = 'niacinamide' approach dies the moment it meets real data. Here are the concrete failure modes, each of which you will hit within the first thousand products.

Synonym fragmentation. The same molecule appears across your catalogue as "Niacinamide," "Nicotinamide," "Vitamin B3," "Nicotinic Acid Amide," "Vitamin PP," and derivative or marketing names like "Niacinamide PC." Every one of these maps to CAS 98-92-0. A literal equality check against the string niacinamide silently misses every non-canonical spelling. A brand that sources from a supplier declaring "Nicotinamide" on the raw material spec sheet will never appear in your filter, even though the product is exactly what the user wants.

Casing and unicode noise. INCI strings arrive from scraped labels, supplier feeds, and manual data entry. You'll see "NIACINAMIDE", "Niacinamide", " niacinamide" with leading whitespace, non-breaking spaces (\u00A0) copied out of PDFs, and trailing punctuation from OCR'd ingredient panels. Each variant defeats naive equality. Even a lowercased comparison breaks on the non-breaking space, because ' niacinamide'.strip won't touch \u00A0 unless you normalize unicode explicitly.

INCI position is not concentration. This is the single most misunderstood nuance, and it breaks any "is there a meaningful amount?" logic. Ingredients present below roughly 1% concentration may legally be listed in any order after the "1% line." Cosmetic chemists writing at Chemists Corner and elsewhere have documented this repeatedly. So a niacinamide sitting seventh on an INCI list could be 3% or it could be 0.1% — INCI position is a dependable concentration proxy only above the ~1% threshold. Below it, position tells you almost nothing.

Marketing names versus regulatory names. Product copy on the PDP says "Vitamin B3 Brightening Serum" while the INCI list declares "Niacinamide." Or a brand lists a proprietary blend name — "ComplexRadiance™" — that hides the active entirely. Your matcher has to reconcile the customer-facing marketing name against the regulatory INCI declaration, and those two vocabularies rarely align.

The downstream consequence is a trust problem. According to the Cosmetic Ingredient Review's safety assessment, cosmetic use concentrations of niacinamide range from as low as 0.0001% in night preparations to about 3% in body and hand lotions. A filter that checks only presence will tag a trace-inclusion night cream right alongside a genuine 5% treatment serum. Worse, many products marketed as "niacinamide" contain levels below those used in most efficacy trials — so a presence-only tag actively misleads users about whether a product does anything.

A niacinamide filter is only as trustworthy as its worst false positive — one mistagged product teaches users to distrust every tag.

All of this points to the same requirement: you need a normalized, synonym-resolved data source that collapses every spelling variant to a single canonical entity. You have two options. You either maintain the synonym and identifier space yourself — a growing pile of regex, a dictionary someone has to update, a mapping table that rots — or you consume it from a structured ingredient index that keeps that space current for you. The rest of this article assumes the latter, because at catalogue scale the maintenance math tips hard against DIY.

Mapping the Niacinamide Namespace: Synonyms, CAS, and Identifiers

Before you write a single line of pipeline code, map the identity space. Every spelling, synonym, and derivative that could appear in an INCI list needs to resolve to one canonical entity. Here is the niacinamide namespace, anchored on chemical identifiers.

Name / Synonym Type CAS Number EC Number Notes
Niacinamide Canonical INCI name 98-92-0 202-713-4 Primary tagging target
Nicotinamide Chemical synonym 98-92-0 202-713-4 Same molecule; IUPAC 3-pyridinecarboxamide
Vitamin B3 Common/marketing name 98-92-0 202-713-4 Appears in product copy, not usually INCI
Nicotinic acid amide Chemical synonym 98-92-0 202-713-4 Older nomenclature
Vitamin PP Legacy synonym 98-92-0 202-713-4 Rare in modern lists
Niacinamide PC Cosmetic-grade derivative 98-92-0 (base) Low-niacin powder; tagging-policy decision

Anchoring on CAS 98-92-0 and EC 202-713-4 beats string matching for one structural reason: identifiers collapse every synonym variant to a single canonical entity. When your resolution layer sees "Nicotinamide," "Vitamin B3," or "Nicotinic acid amide," it doesn't need to recognize the words — it looks up the identifier attached to that ingredient record and finds they all carry the same CAS number. According to Sigma-Aldrich's niacinamide specifications, the canonical identifiers are CAS 98-92-0, EC 202-713-4, and the IUPAC name 3-pyridinecarboxamide. Build your tagging logic on those identifiers and you stop maintaining an ever-growing regex that has to anticipate spellings you haven't seen yet.

The derivative case deserves a deliberate decision. Personal-care ingredient supplier documentation describes Niacinamide PC as cosmetic-grade niacinamide with low niacin content — a white, crystalline, odorless powder freely soluble in water and alcohol. It functions as niacinamide but carries a distinct trade name. Whether it counts toward your has_niacinamide tag is a tagging policy decision, not a data question, and the two use cases point different directions. For a consumer-facing filter, most teams include functional derivatives, because a shopper looking for niacinamide benefits doesn't care about the trade name. For a compliance or formulation tool, you may want derivatives flagged separately so a formulator can distinguish the base active from a branded variant. A structured ingredient data source returns these identifiers plus the synonym set programmatically, which is exactly why identifier-based resolution scales past a hardcoded list.

Choosing Your Tagging Approach: Regex vs. Dictionary vs. API

Three architectures can power a niacinamide tag. They differ sharply on maintenance and on what metadata they can return. Compare them on capability, not on a made-up winner column.

Capability Hand-rolled regex Static synonym dictionary Ingredient data API
Synonym coverage Manual, brittle Good if maintained Broad, maintained upstream
Maintenance burden High per edge case High (manual updates) Low (consumed as service)
New/derivative ingredients No Only after manual add Yes, as index updates
Safety/comedogenicity metadata No No unless hand-built Yes
Identifier resolution (CAS/EC) No Only if mapped Yes
Latency at catalogue scale Fast but limited Fast Sub-100ms median lookups

Each approach is defensible under specific conditions, so match the tool to the problem rather than reaching for the heaviest option by default.

Hand-rolled regex is fine for a tiny, fixed catalogue where you control every INCI string and only ever need to detect one active. A boutique brand with 30 SKUs and an in-house formulator who owns every ingredient panel can ship re.search(r'niacinamide|nicotinamide', inci, re.I) and move on. It rots the moment a new synonym appears, a supplier reformulates, or you ingest a third-party feed with spellings you didn't anticipate. Every edge case becomes a code change and a deploy.

A static synonym dictionary handles the synonym problem better — you map every known variant to a canonical key once. But someone owns that dictionary forever. It goes stale the moment a supplier reformulates or a new derivative enters the market, and you typically find out from a support ticket rather than from your data. It also returns nothing beyond the tag itself: no comedogenicity score, no irritancy signal, no safety status, unless you hand-build all of that metadata too.

An ingredient data API absorbs the churn for you. The ingredient universe is large and constantly moving — the OECD's high-production-volume dossier estimated global nicotinamide production at roughly 15,000 tonnes per year, a commodity used across thousands of products, with new launches and reformulations happening continuously. A maintained index tracks that movement so you don't. A structured API can resolve ingredients at sub-100ms median latency, and a credit-on-successful-match billing model means you don't pay for lookups that return nothing — useful when your parsed INCI arrays are full of noise tokens that won't match anything.

Your synonym dictionary is out of date the moment a supplier reformulates — the question is whether you find out from your data source or from a support ticket.

The practical guidance: for anything above a few dozen static products, or anywhere you'll eventually want richer filters like low-comedogenic or low-irritancy variants of skincare products with niacinamide, the API path avoids rebuilding infrastructure you'd otherwise have to maintain by hand.

Building the Tagging Pipeline: From INCI String to Filterable Tag

Here is the pipeline as sequential engineering steps. It assumes a batch ingredient analyzer endpoint (POST /v1/analyze) that accepts an array of ingredient names and returns, per ingredient, severity labels, comedogenicity and irritancy scores on a 0–5 scale, CAS/EC identifiers, synonyms, and an overall safety_status.

  1. Parse the raw INCI string into a token array. Split on commas, trim whitespace, normalize casing to a consistent form (lowercase is safe for matching), and strip stray unicode — non-breaking spaces, zero-width characters, trailing punctuation from scraped labels. Run a Unicode NFKC normalization pass so \u00A0 collapses to a regular space. Output: a clean string[] per product, e.g. ["aqua", "niacinamide", "glycerin", "zinc pca"].
  2. Batch products into the analyzer. Send the token array to POST /v1/analyze rather than looping single-ingredient lookups. Batching cuts round-trips and cost, and because credit-on-successful-match billing only charges for tokens that resolve, the inevitable noise tokens ("aqua," "parfum," a misspelled filler) don't inflate your bill. One call per product, not one call per ingredient.
  3. Read back the structured response. For each ingredient in the response, capture the canonical name, CAS/EC identifiers, the synonym set, the comedogenicity and irritancy scores (0–5), and safety_status. Keep the full payload in memory for the current product — you'll persist a subset shortly.
  4. Resolve synonyms to a canonical entity. Match any returned identifier of CAS 98-92-0 to your canonical "niacinamide" entity. This is where "Nicotinamide," "Vitamin B3," and — per policy — "Niacinamide PC" collapse into a single tag. You match on the identifier, not the string, so a spelling you've never seen still resolves correctly as long as the API attaches the right CAS number.
  5. Apply your tagging policy. Decide the derivative rule (include Niacinamide PC or flag it separately) and any threshold heuristic. If you want a "meaningful concentration" signal, you can treat an above-the-1%-line position as a weak indicator — but respect the limits from earlier: below ~1%, INCI order is meaningless, so never present position as exact concentration.
  6. Write the tag and its metadata. Persist has_niacinamide = true along with the resolved comedogenicity and irritancy scores and safety_status. Storing the metadata now — not just the boolean — means richer filters later ("low-comedogenic niacinamide products") come for free, without re-querying the API.

The safety_status field matters more than it looks. The Cosmetic Ingredient Review's assessment found niacinamide relatively non-toxic and not a significant skin irritant, which means most niacinamide products will carry low irritancy scores. That's not just a safety note — it's a marketable filter signal you can surface to users later.

Close-up of a code editor showing a Python SDK snippet calling the batch analyze endpoint with an array of ingredient names; dark theme, syntax highlighting visible, developer's hand near keyboard.

Storing and Indexing Tags for Fast Filter Queries

How you store the resolved tag determines whether your filter returns instantly or crawls at catalogue scale. Get the schema and indexing right at write-time and the read-time filter stays cheap.

  • Schema design: boolean flag vs. tag table vs. JSONB metadata. A simple has_niacinamide BOOLEAN column is fastest to query but can't express richness — it's a yes/no with no room for scores or identifiers. A tag or junction table scales to hundreds of actives without schema churn, since adding a new filterable ingredient is a row, not a migration. A JSONB metadata column stores the full resolved payload — scores, CAS/EC identifiers, safety_status — alongside the flag. The hybrid wins: keep a queryable boolean or tag column for fast filtering plus a JSONB column for the resolved detail you'll want when building sub-filters.
  • Storing the resolved metadata, not just the boolean. Persist the comedogenicity and irritancy scores (0–5) and safety_status with each tagged product. The payoff is that you can later build "low-comedogenic niacinamide products" or "niacinamide products safe for sensitive skin" filters without re-hitting the API for data you already fetched. Because the Cosmetic Ingredient Review assessed niacinamide as relatively non-toxic and non-irritant up to 10%, most niacinamide products will land at low irritancy scores — a genuinely useful, marketable signal you get essentially for free by storing what you already resolved.
  • Re-tagging cadence. Ingredient data is not static. Reformulations and new product launches are constant, and the OECD's ~15,000 tonnes/year commodity scale for nicotinamide underscores just how large and active the moving catalogue is. Schedule a re-tag job that recomputes tags when the upstream data source updates or when a product's INCI list changes. A tag written once and never revisited will silently drift out of sync with reality — never assume a fixed ingredient universe.
  • Indexing for query speed. Index the boolean or tag column — a B-tree for a boolean column, a GIN index for JSONB or tag arrays — so the filter returns instantly even at full catalogue scale. The architectural principle: push the expensive resolution work to write-time (tagging), which keeps read-time (user filtering) cheap. Resolving synonyms on every user query is the opposite pattern, and it will not survive traffic.
Flat-lay of several skincare products (serum dropper bottle, cream jar, lotion tube) with visible "niacinamide" / "Vitamin B3" on labels, arranged on a clean neutral surface — represents the end-result feature.

Extending Beyond Niacinamide: A Reusable Active-Ingredient Filter Pattern

The niacinamide pipeline you just built is not a one-off. The same architecture works for retinol, vitamin C, salicylic acid, and hyaluronic acid — you swap the canonical target and its policy rules, and nothing else in the pipeline changes. The parse step, the batch analyze call, the identifier resolution, the storage schema, and the index all stay identical. What varies per active is a small configuration object.

Design that abstraction as a config-driven "active ingredient definition" containing four things: the canonical name, the accepted synonyms and identifiers (anchored on CAS/EC), a derivative policy, and an optional concentration or threshold heuristic. Adding a new filterable active becomes a config entry, not a new code path. Your engineering team ships one pipeline and your product team defines dozens of filters against it.

The niacinamide config works as the reference example:

  • Canonical: Niacinamide (CAS 98-92-0)
  • Synonyms: Nicotinamide, Vitamin B3, Nicotinic acid amide, Vitamin PP
  • Derivative policy: include Niacinamide PC
  • Threshold note: leave-on "active" levels are typically cited at 2–5%

That 2–5% band is not a guess — it's where dermatologist guidance converges. Dr. Heather D. Rogers, MD, founder of Doctor Rogers Skincare, notes niacinamide is effective at concentrations as low as 2%, with most clinical improvements landing between 2–5%, and recommends 2–4% for daily use to balance efficacy against irritation risk. Dr. Fricke of Alamo Heights Dermatology advises beginners toward 2–5% and notes that up to 10% can help more stubborn issues at the cost of tolerability. Dr. Malik, in a Today.com feature, similarly recommends 2–5% for daily use and reserves 10% for persistent concerns like severe hyperpigmentation. On the clinical side, a Practical Dermatology review summarizes trials showing 2% niacinamide reduces sebum and pore size over four weeks, and 4–5% gels reduce acne lesions comparably to 1–2% clindamycin. Those bands can inform an optional "treatment-strength" sub-tag if your data supports concentration signals — with the standing caveat that INCI position rarely gives you exact concentration below the 1% line.

Multi-active filtering falls out of the design for free. Users often want products containing niacinamide and vitamin C, or niacinamide and retinol. Because you stored resolved metadata per product rather than a bare boolean, that request becomes a compound query over your tag columns — no re-analysis, no extra API calls, just a WHERE has_niacinamide AND has_retinol against indexed columns.

Conflict and stacking flags are where stored metadata pays off most. Dermatology guidance is clear that higher niacinamide concentrations, especially stacked with other actives, can raise irritation risk. Midcounty Dermatology frames 5–10% as a "sweet spot" for many people but cautions that higher is not inherently better and that combining actives can provoke irritation. Dr. Rogers warns that 10–20% formulations marketed for faster results can cause stinging, redness, or flushing in people with a weakened barrier. Because you stored per-ingredient irritancy scores (0–5), a mature filter can surface an "irritancy stacking" advisory when a formulation combines multiple high-irritancy actives — turning your metadata into a safety feature, not just a search feature.

Reframed this way, the niacinamide filter is the first instance of a platform capability. You didn't build a niacinamide feature. You built an active-ingredient filtering engine and instantiated it once.

UI mockup of a faceted ingredient filter in a skincare app — a sidebar with toggles for Niacinamide, Vitamin C, Retinol, Salicylic Acid, Hyaluronic Acid, with Niacinamide switched on and a product grid updating. Clean mobile app aesthetic.

Pre-Launch Checklist: Validating Your Niacinamide Filter Before It Ships

Run this QA pass before you enable the filter for skincare products with niacinamide in production. Each item is one verification with a concrete pass condition — copy it into your ticketing system and check every box.

  1. Spot-check known-positive products. Pull 10–15 products you know contain niacinamide and confirm each carries has_niacinamide = true. Pass condition: zero false negatives on obvious cases. If a product you personally know contains the active isn't tagged, your resolution layer has a gap.
  2. Test synonym coverage. Verify products listing "Nicotinamide," "Vitamin B3," and "Nicotinic acid amide" all resolve and tag correctly via CAS 98-92-0. Pass condition: no synonym variant slips through untagged. This is the failure mode that quietly shrinks your result set.
  3. Confirm derivative policy behaves as decided. If your policy includes Niacinamide PC, confirm those products tag; if it excludes them, confirm they don't. Pass condition: behavior matches the documented decision. Write the decision down so it isn't re-litigated in three months by someone who forgot.
  4. Validate the threshold heuristic honestly. If you tag a "meaningful concentration" signal, confirm it does not overclaim. INCI position is a reliable concentration proxy only above the ~1% line. Pass condition: ambiguous mid-list cases are flagged as uncertain, not asserted as treatment-strength.
  5. Check false-positive edge cases. Confirm trace-inclusion products — niacinamide sitting far down the list — are handled per your policy and not silently promoted as treatment-strength. Pass condition: a night cream with 0.0001% niacinamide is not presented identically to a 5% serum.
  6. Load-test filter query latency. Run the filter against the full catalogue and confirm sub-second response with the boolean or tag index in place. Pass condition: the query plan shows the index is actually used, not a sequential scan. An index that exists but isn't hit is a false sense of security.
  7. Verify the re-tagging job on data refresh. Trigger a simulated upstream data update or an INCI edit and confirm the re-tag job recomputes affected tags without manual intervention. Pass condition: tags update automatically, and stale tags do not persist after the source changes.
  8. QA the empty state and metadata display. Confirm the UI handles zero results gracefully and that stored comedogenicity, irritancy, and safety_status metadata renders correctly for any richer sub-filters. Pass condition: no blank screen on an empty result, and no null-rendering artifacts in the metadata display.

FAQ

Does the position of niacinamide on the INCI list tell me the concentration?

Not reliably. Ingredients present below roughly 1% may be listed in any order after the "1% line," so a mid-list niacinamide could be 3% or 0.1% — the position alone won't distinguish them. Cosmetic chemists have documented this rule repeatedly, and it's the reason naive position-based logic misleads. INCI position becomes a dependable concentration proxy only above that ~1% threshold. Treat it as a weak signal at best, never as an exact concentration. If you need real concentration data for a treatment-strength sub-tag, you need the underlying formulation figures, which INCI declarations rarely expose directly.

Should I tag products with derivatives like Niacinamide PC as "niacinamide products"?

It's a policy decision, not a data one. Ingredient-supplier documentation describes Niacinamide PC as cosmetic-grade niacinamide with low niacin content and the same core functionality. For consumer-facing filters, most teams include it, because a shopper wants the benefit and doesn't care about the trade name. For compliance or formulation tooling, you'll often want it flagged separately so a formulator can distinguish the base active from a branded variant. Whichever you choose, encode the rule in your active-ingredient config and document it, so the behavior is explicit and doesn't get quietly reversed by a future change.

How often do I need to re-tag my catalogue?

As often as your ingredient data or product INCI lists change. Niacinamide is a high-volume commodity — the OECD estimated roughly 15,000 tonnes per year of nicotinamide production — used across thousands of products that are continuously reformulated and relaunched. Treat the ingredient universe as moving, not fixed. A scheduled re-tag job tied to upstream data updates, plus a trigger on any product INCI edit, keeps your tags accurate without manual intervention. A tag written once and never revisited will drift out of sync with the actual formulations on your shelves.

Can the same pipeline handle proprietary or trademarked ingredient blends?

Only to the extent the blend's INCI components are disclosed. If a proprietary name masks the actives entirely, no matcher can resolve them from the trade name alone — the information simply isn't in the string. When the underlying INCI components are declared, the same synonym and identifier resolution applies exactly as it does for any other ingredient. The right default is to flag undisclosed blends as "unresolved" rather than silently omitting them, so your data team can see the gap and decide how to handle it rather than shipping a false negative.