shoc-backend/docs/spikes/search-strategy.md

6.8 KiB
Raw Blame History

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
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)

  1. Index seek em IX_workOrders_ScheduledDate (proposto) com range [scheduledFrom, scheduledTo+1day)
  2. Filtro istemplate != true
  3. Filtros exatos em AssignTo, LocationId, Status (AND)
  4. Sem search textual no servidor se search vazio
  5. Join LEFT Locations, AssignToUser, LEFT Dispatches + Vendor (Fase 1 board — vendor no DTO, não no search)
  6. Projeção flat para WorkOrderBoardRowDto — sem subquery lastUpdated

Caminho search servidor (count > 300 ou user digitou termo indexável)

  1. Mesma janela + filtros exatos
  2. AND (InternalWONumber LIKE 'term%' OR SiteCode LIKE 'term%')
  3. Se count ainda > 300 → HTTP 400 com mensagem "Refine search or narrow date window"

Anti-padrões proibidos no board endpoint

  • pageSize=500 como gambiarra de fetch semanal
  • Contains('%x%') em múltiplas tabelas sem janela temporal
  • Filtro assignee por nome concatenado (FirstName + LastName) — usar assigneeId
  • 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:

  1. Janela ScheduledDate obrigatória + índice dedicado
  2. Search servidor mínimo (StartsWith em identificadores)
  3. Fallback client-side para campos ricos quando count ≤ 300
  4. Vendor/tech e full-text adiados para Fase 2

Esta estratégia desbloqueia GetWorkOrdersBoard sem replicar os anti-padrões de GetWorkOrderListPagedAsync.