6.8 KiB
Spike Search Strategy — GetWorkOrdersBoard
Data: 2026-06-19
Gate: G3 (pré-requisito Fase 1)
Objetivo: Definir busca mínima viável para o Schedule Board sem degradar performance.
Estado atual (GetWorkOrderList)
Implementação em SeaHaven.DataServices/Implementation/WorkOrderDataService.cs (GetWorkOrderListPagedAsync):
| Aspecto | Comportamento atual |
|---|---|
| Paginação | page / pageSize (default 12), sem cap |
| Janela temporal | Ausente — sem filtro ScheduledDate |
| Search | Contains + ToLower() em 5 campos |
| Campos search | InternalWONumber, WorkerOrderNumber, WorkerOrderTitle, Locations.Title, Locations.Name |
| Campos omitidos | SiteCode, Trade, Problem, vendor, dispatcher por ID |
| Joins | Locations, AssignToUser (AspNetUsers) |
| Vendor/dispatch | Não incluídos |
| Projeção extra | Subquery lastUpdated via Comments + WorkOrderAuditLogs por linha |
Padrão SQL gerado (search)
WHERE LOWER(InternalWONumber) LIKE '%term%'
OR LOWER(WorkerOrderNumber) LIKE '%term%'
OR LOWER(WorkerOrderTitle) LIKE '%term%'
OR LOWER(Locations.Name) LIKE '%term%'
...
Leading wildcard → table scan em colunas nvarchar(max).
Respostas às perguntas do spike
| # | Pergunta | Decisão Fase 1 |
|---|---|---|
| S1 | Volume típico por semana | Assumir < 200 WOs/semana até medir em prod; threshold fallback = 300 |
| S2 | Campos pesquisáveis mínimos | internalWONumber, siteCode, assigneeId[] (exato), locationId |
| S3 | LIKE vs full-text | Sem full-text Fase 1; StartsWith em WO# e siteCode; evitar Contains em title/location no servidor |
| S4 | Escopo | Search sempre scoped a scheduledFrom / scheduledTo (obrigatório no endpoint) |
| S5 | Vendor/tech no search | Fase 2 — exige join Dispatches → Vendor |
Estratégia em 2 camadas
Request GetWorkOrdersBoard
│
├─► 1. Filtro janela ScheduledDate (obrigatório)
│
├─► 2. Filtros exatos: status, assigneeId[], locationId
│
├─► 3. COUNT no escopo
│
├─► count <= 300?
│ ├─ SIM → retornar semana inteira (sem paginação Fase 1)
│ │ search textual extra no FE: title, locationLabel, serviceType
│ └─ NÃO → exigir critério indexável no servidor
│ (woNumber ou siteCode StartsWith)
│
└─► Response: { scheduledFrom, scheduledTo, totalCount, items[] }
Campos Fase 1 vs Fase 2
| Campo | Fase 1 (servidor) | Fase 1 (client fallback) | Fase 2 |
|---|---|---|---|
internalWONumber / workerOrderNumber |
StartsWith |
— | — |
siteCode |
StartsWith ou igualdade |
— | — |
assigneeId |
multi-select exato | — | — |
locationId |
exato | — | — |
status |
exato / multi | pills client-side | — |
workerOrderTitle |
— | client-side se count ≤ 300 | server Contains opcional |
locationLabel |
— | client-side | — |
serviceType (trade/category) |
— | client-side | server join |
vendorCompany / vendorTechnician |
— | — | join Dispatch→Vendor |
assigneeName |
— | client-side | evitar join por nome |
Query plan esperado (Fase 1)
Caminho feliz (semana típica, ≤ 300 rows)
- Index seek em
IX_workOrders_ScheduledDate(proposto) com range[scheduledFrom, scheduledTo+1day) - Filtro
istemplate != true - Filtros exatos em
AssignTo,LocationId,Status(AND) - Sem search textual no servidor se
searchvazio - Join LEFT
Locations,AssignToUser, LEFTDispatches+Vendor(Fase 1 board — vendor no DTO, não no search) - Projeção flat para
WorkOrderBoardRowDto— sem subquerylastUpdated
Caminho search servidor (count > 300 ou user digitou termo indexável)
- Mesma janela + filtros exatos
- AND (
InternalWONumber LIKE 'term%'ORSiteCode LIKE 'term%') - Se count ainda > 300 → HTTP 400 com mensagem "Refine search or narrow date window"
Anti-padrões proibidos no board endpoint
pageSize=500como gambiarra de fetch semanalContains('%x%')em múltiplas tabelas sem janela temporal- Filtro assignee por nome concatenado (
FirstName + LastName) — usarassigneeId - Subquery correlated Comments/AuditLog por row
Índices recomendados
Migration separada após aprovação desta spike (não incluir na Fase 1 code sem review):
| Índice | Coluna(s) | Prioridade | Nota |
|---|---|---|---|
IX_workOrders_ScheduledDate |
ScheduledDate |
P1 | Filtro de janela do board |
IX_workOrders_InternalWONumber |
InternalWONumber |
P2 | Requer alterar coluna de nvarchar(max) → nvarchar(50) |
IX_workOrders_SiteCode |
SiteCode |
P2 | Idem — tamanho fixo ~50 |
IX_workOrders_AssignTo |
AssignTo |
existente | Multi-select dispatcher |
IX_workOrders_LocationId |
LocationId |
existente | Filtro location |
Full-text index: não recomendado Fase 1 — volume semanal baixo + fallback client-side suficiente.
Referência de implementação
Copiar padrão de janela temporal de DispatchController.List:
if (dateFrom.HasValue)
q = q.Where(d => d.DispatchedAt >= dateFrom.Value);
if (dateTo.HasValue)
{
var end = dateTo.Value.Date.AddDays(1);
q = q.Where(d => d.DispatchedAt < end);
}
Adaptar para WorkOrder.ScheduledDate no board endpoint.
Parâmetros propostos — GetWorkOrdersBoard
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
scheduledFrom |
DateOnly |
Sim | Início da semana/janela |
scheduledTo |
DateOnly |
Sim | Fim da janela (inclusivo) |
search |
string |
Não | WO# ou siteCode prefix |
assignee |
string[] |
Não | IDs de dispatcher (multi) |
status |
string[] |
Não | Status canônico API |
locationId |
int? |
Não | Filtro location |
Limite hard janela: max 90 dias (Fase 2 advanced search cross-week).
Threshold e fallback
| Constante | Valor | Justificativa |
|---|---|---|
BoardSearchClientSideThreshold |
300 | Payload ~300 rows × ~500 bytes ≈ 150 KB — aceitável para 1 call/semana |
| Sem paginação Fase 1 | — | Janela semanal é o filtro natural |
| Paginação Fase 2 | cursor/offset | Só quando janela > 1 semana com search global |
Conclusão
Fase 1 adota window-first, search-second:
- Janela
ScheduledDateobrigatória + índice dedicado - Search servidor mínimo (
StartsWithem identificadores) - Fallback client-side para campos ricos quando count ≤ 300
- Vendor/tech e full-text adiados para Fase 2
Esta estratégia desbloqueia GetWorkOrdersBoard sem replicar os anti-padrões de GetWorkOrderListPagedAsync.