Skip to content

Marketplace & Search (SRS-MKT)

Purpose

Owns how customers discover vendors: the category taxonomy, browse/category pages, the Meilisearch-backed search experience (filters, sorting, typo tolerance, AR/EN synonyms), featured-vendor placement, and empty-result handling. It reads published vendor listings (owned by Vendor Listings) and never writes vendor content. The search index is a derived, rebuildable projection — MySQL remains the source of truth (Platform domain owns the indexing pipeline).

Actors

Actor Description
Guest Can browse and search everything without an account.
Customer Same as guest, plus availability-date filtering tied to their event and saving favorites.
Vendor Consumes marketplace only as a preview of their own placement.
Admin (Content) Manages taxonomy, synonyms, featured slots, and search curation.
System Index sync workers, ranking jobs.

User stories

  • As a guest, I want to browse "قاعات / Venues" in my city, so that I can see options before creating an account.
  • As a customer, I want to filter photographers by budget, area, and my event date, so that I only see vendors I can actually book.
  • As a customer typing in Arabic, I want "مصور" to also match "photography", and typos like "فوتغراف" to still find results, so that spelling never blocks discovery.
  • As a customer, I want to sort by rating or price, so that I can compare on what matters to me.
  • As a vendor on Professional, I want my listing featured in my category, so that I get more leads.
  • As a content admin, I want to reorder categories and manage synonyms, so that discovery quality keeps improving without deployments.

Functional requirements

Category taxonomy

ID Requirement Phase
SRS-MKT-001 The platform ships a two-level taxonomy: top-level categories and optional subcategories, each with name_ar, name_en, ASCII slug, icon, sort_order, status (active/inactive), and SEO meta (AR/EN). Taxonomy is admin-managed data; changes require no deployment. MVP
SRS-MKT-002 Launch taxonomy (initial content, admin-editable): see table below. Every vendor maps to 1–3 categories (SRS-VOS-004). MVP
SRS-MKT-003 Deactivating a category hides it from browse/search but keeps vendor associations; vendors in a deactivated category are prompted to pick a replacement. A category with active vendors cannot be hard-deleted. MVP
SRS-MKT-004 Category pages: /{locale}/c/{slug} — hero with bilingual name, vendor count, filter bar, featured strip, then result grid. Fully server-rendered for SEO (Next.js). MVP

Launch categories:

Slug name_en name_ar Example subcategories
venues Venues القاعات وأماكن الفعاليات Wedding halls, hotels, gardens, restaurants, chalets
catering Catering & Hospitality الضيافة والتموين Buffets, live cooking, desserts, coffee stations (قهوجي)
photography Photography التصوير الفوتوغرافي Weddings, corporate, products
videography Videography تصوير الفيديو Event films, live streaming, drone
decor-lighting Decor & Lighting الديكور والإضاءة Stage (كوشة), flowers arrangement, balloons, lighting design
entertainment Entertainment & DJ الترفيه والعروض DJs, bands (فرقة), shows, MCs
flowers Flowers الورود والتنسيق Bouquets, arch design, giveaways
transportation Transportation النقل والمواصلات Luxury cars, guest shuttles, valet
planning Event Planning تنظيم وإدارة الفعاليات Full planning, on-day coordination
invitations-printing Invitations & Printing الدعوات والطباعة Card design, digital invitations, giveaways printing
security Security الأمن والحراسة Crowd control, VIP protection
cleaning Cleaning التنظيف Pre/post event cleaning
kids-entertainment Kids Entertainment ترفيه الأطفال Inflatables, characters, face painting

Browse & category pages

ID Requirement Phase
SRS-MKT-010 Marketplace home: category grid (icon + bilingual name + vendor count), featured vendors strip, top-rated strip, recently-joined strip. All strips are horizontally scrollable and RTL-aware. MVP
SRS-MKT-011 Result cards show: cover image, business name (locale-preferred), primary category, area/governorate, rating average (1 decimal) + review count, starting price ("from KWD 250.000"), verified badge, featured badge where applicable. MVP
SRS-MKT-012 Infinite scroll on mobile, numbered pagination on web (SEO); 20 results per page. MVP
SRS-MKT-013 Favorites: authenticated users can heart any vendor from cards or profiles; favorites sync across devices. Guests tapping the heart get a login sheet that returns them to the same scroll position after auth. MVP

Search & filters

ID Requirement Phase
SRS-MKT-020 Global search box (header, home, category pages) queries Meilisearch over published vendors: fields indexed = business names (AR+EN), descriptions (AR+EN), category names + synonyms, package names, areas served. Search-as-you-type with ≤ 100 ms p95 (SRS-NFR-002). MVP
SRS-MKT-021 Filters (combinable, all reflected in the URL query string for shareability): city/governorate + area (Kuwait launch: Al Asimah, Hawalli, Farwaniya, Mubarak Al-Kabeer, Ahmadi, Jahra; areas as second level), budget range (KWD min–max slider over listing starting price), rating (≥ 3, ≥ 4, ≥ 4.5), category/subcategory, availability date (vendors whose calendar is open on that date, SRS-LST-040). MVP
SRS-MKT-022 Sorting options: Relevance (default when a text query exists), Rating (Bayesian-weighted, see business rules), Price low→high / high→low (starting price), Distance (requires user location permission or a chosen area centroid; falls back to Relevance if unavailable). Default with no query: Featured, then rating. MVP
SRS-MKT-023 Typo tolerance per Meilisearch defaults tuned for Arabic: 1 typo allowed for words ≥ 5 chars, 2 for ≥ 9; typo tolerance disabled on business_name exact-phrase queries in quotes. MVP
SRS-MKT-024 AR/EN synonym dictionary maintained by content admins, applied at index level. Seed set includes: قاعة ↔ hall ↔ venue; مصور ↔ photographer; ضيافة ↔ catering ↔ بوفيه; دي جي ↔ DJ ↔ منسق أغاني; كوشة ↔ stage decor; زفاف ↔ wedding ↔ عرس. Also Arabizi transliterations (e.g. "9ala" tolerated via typo rules, not synonyms). MVP
SRS-MKT-025 Index sync: create/update/publish/unpublish/suspend of a vendor or listing MUST reflect in the index within 60 s (queued jobs); nightly full-reconciliation job repairs drift. MVP
SRS-MKT-026 Ranking rules order (Meilisearch): words → typo → proximity → attribute → sort → exactness, with a custom ranking attribute boost_score (featured status + quality score + Bayesian rating) applied before exactness. MVP
SRS-MKT-027 Search analytics: every query logs (anonymized) query text, locale, filters, result count, and clicked position — feeding zero-result reports and the Scale recommendation engine. MVP
SRS-MKT-028 AI-recommended sort ("Recommended for you"): personalized ranking from event context, budget, and behavior (Intelligence domain). Scale
ID Requirement Phase
SRS-MKT-030 Vendors on Professional/Enterprise are eligible for featured placement: a "مميز / Featured" badge and priority slots (top strip on home + top 3 grid positions in their primary category, visually labeled). MVP
SRS-MKT-031 Featured slots per category are capped (launch: 6); when eligible vendors exceed slots, rotation is round-robin per 24 h so all eligible vendors get fair exposure. Enterprise outranks Professional in rotation weight 2:1. MVP
SRS-MKT-032 Featured placement never overrides filters: a featured vendor that doesn't match the active filters does not appear. Sponsored/paid placements beyond plan-based featuring Scale

Empty results & discovery aids

ID Requirement Phase
SRS-MKT-040 Zero-result searches MUST show, in order: (1) "no exact matches" message with the query echoed, (2) auto-relaxed results (drop the most restrictive filter, labeled "close matches — we removed: {filter}"), (3) top vendors in the nearest matching category. Never a dead end. MVP
SRS-MKT-041 Zero-result queries with volume ≥ 10/week surface in an admin report to drive synonym/taxonomy/vendor-recruitment decisions. MVP
SRS-MKT-042 Recent searches (device-local, last 5) and trending categories shown on empty search focus. MVP

Business rules

  1. Only vendors that are approved, published, and not suspended are indexed. Grace-period vendors remain indexed; expired-to-Free vendors remain indexed with Free-tier content only.
  2. Rating sort uses a Bayesian average: (v·R + m·C) / (v + m) with m = 5 (prior weight) and C = platform-wide mean rating, preventing a single 5-star review from outranking established vendors.
  3. "Starting price" on cards = the lowest active package price of the listing; listings with no packages show "request a quote" instead of a price and sort last in price sorting.
  4. Distance sorting uses the vendor's HQ pin at MVP (branch-aware at Scale).
  5. Availability-date filtering treats vendors with no calendar configured as available (opt-out model) — configured calendars are authoritative (SRS-LST-040).
  6. Search results never expose vendor contact details (phone/email) — contact happens through RFQs and in-app messaging (platform integrity rule).
  7. Locale affects display language and synonym weighting but never filters the vendor pool: an Arabic-only listing is still findable from the English UI (with fallback display per the bilingual convention in the overview).

Validation rules

Input Rule
Search query 1–120 chars; stripped of control chars; HTML-escaped in echo; queries < 2 chars return trending instead of searching.
Budget range 0.000 ≤ min ≤ max ≤ 99999.999 KWD; server clamps invalid ranges.
Governorate/area Must exist in the areas reference table (seeded from Kuwait's 6 governorates + PACI area list).
Availability date Valid date, today ≤ date ≤ today + 24 months.
Rating filter One of 3, 4, 4.5.
Sort key Whitelist: relevance, rating, price_asc, price_desc, distance, recommended (Scale); unknown keys fall back to default.
Category slug Must resolve to an active category, else 404 with category-suggestion page.
Page/cursor Max page depth 500 results via API; beyond that, prompt to refine filters.

Permissions matrix

Action Guest Customer Vendor Admin (Content) Admin (Super)
Browse & search
Filter by availability date
Save favorites ✔ (as customer context)
Preview own search placement
Manage taxonomy (CRUD, ordering)
Manage synonym dictionary
Configure featured slot caps / rotation
View search analytics & zero-result reports

Status / state machine

Marketplace entities have simple lifecycles; the only stateful entity owned here is the category:

stateDiagram-v2
    [*] --> Active: admin creates (both locales required)
    Active --> Inactive: admin deactivates (hidden from browse/search)
    Inactive --> Active: admin reactivates
    Inactive --> Archived: no vendor associations remain
    Archived --> [*]

Vendor/listing visibility states are owned by Vendor Listings and Vendor Onboarding; this module only consumes the derived is_searchable flag.

Edge cases

  • Mixed-script queries ("wedding قاعة"): tokenized per-script; both tokens searched across both language fields.
  • All vendors in a category are Free-plan (no featured eligibles): featured strip collapses entirely rather than showing an empty rail.
  • Index unavailable (Meilisearch down): browse pages fall back to MySQL category queries (rating-sorted, no text search); the search box shows a degraded-mode notice. Circuit breaker with 30 s probes.
  • Vendor suspended mid-session: cached result card may still be visible; profile navigation returns the "vendor unavailable" page, and the next index sync (≤ 60 s) removes the card.
  • Price filter with quote-only listings: excluded when a budget filter is active (no price to compare), included otherwise.
  • Duplicate businesses gaming rating sort: one license = one vendor (SRS-VOS business rule 1) plus Bayesian weighting limits the exploit surface.
  • Area chosen but distance sort requested without geolocation: use the area centroid; if neither exists, silently fall back to relevance with an explanatory toast.
  • Deep pagination scraping: page depth cap (validation table) + per-IP rate limits.

Empty / loading / error states

Screen Empty Loading Error
Marketplace home Impossible for categories (seeded); strips hide when < 3 items Skeleton category grid + card skeletons Cached-content banner "showing recent results"; pull-to-refresh
Category page "No vendors here yet — be the first!" + vendor-signup CTA + nearby categories Skeleton cards ×8, filter bar interactive immediately Inline retry; filters preserved
Search results SRS-MKT-040 three-tier fallback Instant-search spinner in box; previous results stay visible until new ones arrive (no flash) Degraded-mode DB fallback; if that fails, full-page retry with preserved query
Favorites "Nothing saved yet" + browse CTA Skeleton cards Retry; un-favorite failures roll back optimistic UI

Acceptance criteria

  • All 13 launch categories render with correct AR/EN names, icons, and RTL layout; category page URLs are locale-prefixed and SEO-rendered.
  • Typing "مصورين اعراس" returns wedding photographers; typing "fotograph" (typo) still returns photography vendors.
  • Each filter works alone and combined; the URL fully reconstructs any filtered view; filters survive login round-trip.
  • Availability-date filter excludes a vendor with that date blocked in their calendar and includes vendors with no calendar.
  • Sort by rating uses Bayesian ordering (verified with a 1-review 5.0 vendor vs. a 200-review 4.7 vendor).
  • Featured rotation: with 8 eligible vendors and 6 slots, all 8 appear across two consecutive days; badges display; filtered-out featured vendors never appear.
  • Publish/unpublish/suspend reflects in search within 60 s.
  • Zero-result flow shows relaxed results and never a dead end; the query appears in the admin zero-result report.
  • Meilisearch outage degrades to DB browse without 5xx to end users.
  • p95 search latency ≤ 100 ms measured over the seeded launch dataset (≥ 500 vendors).

Data entities touched

categories, subcategories, areas (governorate/area reference), vendor_category, vendors (read), listings + packages (read, for starting price), favorites, featured_rotations, search_synonyms, search_query_logs, Meilisearch indexes: vendors_index (derived).

GET    /api/v1/categories
GET    /api/v1/categories/{slug}
GET    /api/v1/areas
GET    /api/v1/search/vendors            (q, filters, sort, cursor)
GET    /api/v1/search/suggestions        (autocomplete)
GET    /api/v1/marketplace/home          (strips: featured, top-rated, new)
GET    /api/v1/vendors/{vendorId}/preview-placement
POST   /api/v1/me/favorites/{vendorId}
DELETE /api/v1/me/favorites/{vendorId}
GET    /api/v1/me/favorites
GET    /api/v1/admin/categories
POST   /api/v1/admin/categories
PATCH  /api/v1/admin/categories/{id}
GET    /api/v1/admin/search/synonyms
PUT    /api/v1/admin/search/synonyms
GET    /api/v1/admin/search/zero-results-report
PATCH  /api/v1/admin/featured-settings