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 |
Featured vendors
| 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
- 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.
- 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.
- "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.
- Distance sorting uses the vendor's HQ pin at MVP (branch-aware at Scale).
- Availability-date filtering treats vendors with no calendar configured as available (opt-out model) — configured calendars are authoritative (SRS-LST-040).
- Search results never expose vendor contact details (phone/email) — contact happens through RFQs and in-app messaging (platform integrity rule).
- 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
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