01 — Product Overview (Search Module)
- 1. What the module is
- 2. Entities searched (from
search-indexer.service.ts:7-32) - 3. What is matched (indexed fields)
- 4. Scope in / scope out
- 5. PRD native-app exclusion (flagged)
- 6. Terminology
Global search across all indexed StudyLyon entities. Derived from
src/modules/search/**(controller, service, indexer, schema, repository, DTO),src/modules/rbac/permissions.constants.ts,src/infrastructure/redis/,docs/IMPLEMENTATION_PLAN.md, andsrc/common/dto/pagination-query.dto.ts. Nothing is invented; plan-only capability is marked(planned), client-only or roadmap-only capability(forward-looking), analytics(proposed).
1. What the module is
A single global search endpoint that searches a dedicated search_indexes
collection (a denormalized mirror of selected business entities) and returns an
aggregated flat list of typed results, paginated and relevance-ranked.
Key facts from source:
- One endpoint:
GET /api/v1/search(@Controller('search')search.controller.ts:11;@Get():15).@ApiTags('search'), bearer auth,JwtAuthGuard(search.controller.ts:8-10). - Guarded by the single
@Permissions('search')permission (search.controller.ts:16; declaredpermissions.constants.ts:93— note: it is a bare'search'string, not asearch.*wildcard family). - Result shape is a flat
SearchResultlist —{ entityType, entityId, title, description, tags }(search.service.ts:10-16). Typed grouping is a client concern; the API is not grouped. - Query params:
q(string, optional),entityType(string, optional),page(≥1, default 1),limit(1–50, default 20) (search-query.dto.ts:5-30). - Empty/whitespace
qreturns{ data: [], meta }withtotalItems: 0(search.service.ts:28-30) — no error, no index scan. - Index maintained event-driven:
SearchIndexerServicesubscribes to every domain event viaeventBus.onAny(search-indexer.service.ts:44-46) and upserts/soft-deletes index rows (search-indexer.service.ts:48-99). - MongoDB full-text (
$text+textScore) with a regex fallback (search-index.repository.ts:20-50); relevance sort by textScore.
2. Entities searched (from search-indexer.service.ts:7-32)
| entityType | Source events | Notes |
|---|---|---|
User | UserCreated, UserUpdated, UserDeleted | index title = name; description = email |
Student | StudentCreated/Updated/Deleted | title falls back to admissionNumber (search-indexer.service.ts:71) |
Teacher | TeacherCreated/Updated/Deleted | tags may include role/department/grade (search-indexer.service.ts:83-86) |
Staff | StaffCreated/Updated/Deleted | same extraction pipeline |
Parent | ParentCreated/Updated/Deleted | same |
Book | BookCreated/Updated/Deleted | library catalogue; description = book description |
Organization | OrganizationCreated/Updated/Deleted | tenant-level entity |
Lead | LeadCreated only (:29) | gap: no LeadUpdated/LeadDeleted mapping → stale/dead lead rows persist |
Announcement | AnnouncementCreated, AnnouncementPublished (:30-31) | gap: no updated/deleted mapping |
Plan-only additions ((planned)): index batches, sessions, test_series, DPP
for the coaching module (docs/IMPLEMENTATION_PLAN.md:771); the plan's
"Global Search" task targets "students, staff, leads, books, announcements"
(docs/IMPLEMENTATION_PLAN.md:174).
3. What is matched (indexed fields)
Per index row: title, description, tags, and a denormalized text
string "${title} ${description} ${tags.join(' ')}"
(search-indexer.service.ts:90-96). The Mongo text index covers
title, description, tags, text (search-index.schema.ts:29-34);
the regex fallback matches the same four fields, case-insensitive substring
(search-index.repository.ts:36-50). Match fields map from source payloads:
title = name → title → admissionNumber → firstName + lastName
(search-indexer.service.ts:68-75); description = email → description →
body (:77-81); tags = role, department, grade (:83-86).
4. Scope in / scope out
| In scope (implemented) | Out of scope / noted |
|---|---|
| Global query across indexed entities, relevance-ranked | Per-entity native search endpoints — module owns a single GET /search |
entityType filter + standard pagination | Result grouping / typed sections — client-side concern |
| Event-driven index sync (create/update/delete for 7 entity types) | External engine (Elasticsearch/Meilisearch) (planned) — none in code |
Tenant isolation on every read/write (base.schema.ts:9-31) | Search result caching — RedisCacheService exists (redis-cache.service.ts:16-19) but is not wired into SearchService (proposed) |
Regex fallback when $text errors | Search history / saved searches / suggestions (forward-looking) |
| Lead/Announcement update-delete sync — known gap (see §2) |
5. PRD native-app exclusion (flagged)
Per the shared ledger A1 (00-shared/12): the PRD puts native mobile apps
out of Phase 1 (PRODUCT_REQUIREMENTS_DOCUMENT.md:144), roadmap Phase 3 =
read-only companion. This package is the forward-looking client spec for
the Flutter app against the v1 API; the backend (controller/service/repo) is
fully implemented and is the only Phase-1 surface. QR-code entry (scan a
book/student barcode into the search bar) is (forward-looking) — no QR
backend support (ledger B4).
6. Terminology
- Index row — one
search_indexesdocument per(tenantId, entityType, entityId)(search-index.schema.ts:7-25). - Scoring — MongoDB
textScoreused for sort only; not returned to the client (search-index.repository.ts:28-33). - Fallback — case-insensitive regex
$orscan used when$textthrows (search.service.ts:35-41).