perf(api): make batch vehicle search exact
This commit is contained in:
@@ -911,6 +911,27 @@ func TestHandlerVehicleRealtimeAcceptsBatchKeywords(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlerVehicleRealtimeBatchKeywordsAreExact(t *testing.T) {
|
||||||
|
handler := NewHandler(NewService(NewMockStore()))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/realtime/vehicles?keywords=%E7%B2%A4AG1831&limit=10", nil)
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Data struct {
|
||||||
|
Items []VehicleRealtimeRow `json:"items"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if len(body.Data.Items) != 0 {
|
||||||
|
t.Fatalf("copied batch plates must not fuzzy-match similar identities, got %+v body=%s", body.Data.Items, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandlerHistoryMileageQualityOps(t *testing.T) {
|
func TestHandlerHistoryMileageQualityOps(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
|
|||||||
@@ -486,8 +486,17 @@ func buildArchiveMissingFieldStats(missingPlate, missingPhone, missingOEM int) [
|
|||||||
func (m *MockStore) VehicleRealtime(_ context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
|
func (m *MockStore) VehicleRealtime(_ context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
|
||||||
rows := m.locations
|
rows := m.locations
|
||||||
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
|
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
|
||||||
|
batch := strings.TrimSpace(query.Get("keywords")) != ""
|
||||||
rows = keep(rows, func(row RealtimeLocationRow) bool {
|
rows = keep(rows, func(row RealtimeLocationRow) bool {
|
||||||
vehicle := m.vehicleByVIN(row.VIN)
|
vehicle := m.vehicleByVIN(row.VIN)
|
||||||
|
if batch {
|
||||||
|
for _, keyword := range keywords {
|
||||||
|
if strings.EqualFold(row.VIN, keyword) || strings.EqualFold(row.Plate, keyword) || strings.EqualFold(vehicle.Plate, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
haystack := strings.ToLower(row.VIN + row.Plate + vehicle.Plate + vehicle.Phone + vehicle.OEM)
|
haystack := strings.ToLower(row.VIN + row.Plate + vehicle.Plate + vehicle.Phone + vehicle.OEM)
|
||||||
for _, keyword := range keywords {
|
for _, keyword := range keywords {
|
||||||
if strings.Contains(haystack, strings.ToLower(keyword)) {
|
if strings.Contains(haystack, strings.ToLower(keyword)) {
|
||||||
|
|||||||
@@ -329,13 +329,20 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
|||||||
args = append(args, protocol)
|
args = append(args, protocol)
|
||||||
}
|
}
|
||||||
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
|
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
|
||||||
matches := make([]string, 0, len(keywords))
|
if strings.TrimSpace(query.Get("keywords")) != "" {
|
||||||
for _, keyword := range keywords {
|
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(keywords)), ",")
|
||||||
matches = append(matches, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)")
|
where = append(where, "(l.vin IN ("+placeholders+") OR l.plate IN ("+placeholders+") OR b.plate IN ("+placeholders+"))")
|
||||||
|
for range 3 {
|
||||||
|
for _, keyword := range keywords {
|
||||||
|
args = append(args, keyword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keyword := keywords[0]
|
||||||
like := "%" + keyword + "%"
|
like := "%" + keyword + "%"
|
||||||
|
where = append(where, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)")
|
||||||
args = append(args, like, like, like, like, like)
|
args = append(args, like, like, like, like, like)
|
||||||
}
|
}
|
||||||
where = append(where, "("+strings.Join(matches, " OR ")+")")
|
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(query.Get("online")) {
|
switch strings.TrimSpace(query.Get("online")) {
|
||||||
case "online":
|
case "online":
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package platform
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -217,17 +218,39 @@ func TestBuildVehicleRealtimeSQL(t *testing.T) {
|
|||||||
|
|
||||||
func TestBuildVehicleRealtimeSQLFiltersMultipleVehicleKeywords(t *testing.T) {
|
func TestBuildVehicleRealtimeSQLFiltersMultipleVehicleKeywords(t *testing.T) {
|
||||||
built := buildVehicleRealtimeSQL(url.Values{"keywords": {"粤AG18312, 川AHTWO1, 粤AG18312"}, "limit": {"10"}})
|
built := buildVehicleRealtimeSQL(url.Values{"keywords": {"粤AG18312, 川AHTWO1, 粤AG18312"}, "limit": {"10"}})
|
||||||
if !strings.Contains(built.Text, ") OR (l.vin LIKE ?") || !strings.Contains(built.CountText, ") OR (l.vin LIKE ?") {
|
for _, text := range []string{built.Text, built.CountText} {
|
||||||
t.Fatalf("batch vehicle search must match any keyword in rows and count: %s / %s", built.Text, built.CountText)
|
if !strings.Contains(text, "l.vin IN (?,?)") || !strings.Contains(text, "l.plate IN (?,?)") || !strings.Contains(text, "b.plate IN (?,?)") {
|
||||||
|
t.Fatalf("batch vehicle search must use exact identity sets in rows and count: %s", text)
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "LIKE") {
|
||||||
|
t.Fatalf("batch vehicle search must not expand copied plates into fuzzy predicates: %s", text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(built.Args) != 12 || built.Args[0] != "%粤AG18312%" || built.Args[5] != "%川AHTWO1%" || built.Args[10] != 10 {
|
if len(built.Args) != 8 || built.Args[0] != "粤AG18312" || built.Args[1] != "川AHTWO1" || built.Args[2] != "粤AG18312" || built.Args[6] != 10 {
|
||||||
t.Fatalf("batch args = %#v", built.Args)
|
t.Fatalf("batch args = %#v", built.Args)
|
||||||
}
|
}
|
||||||
if len(built.CountArgs) != 10 || built.CountArgs[0] != "%粤AG18312%" || built.CountArgs[5] != "%川AHTWO1%" {
|
if len(built.CountArgs) != 6 || built.CountArgs[0] != "粤AG18312" || built.CountArgs[1] != "川AHTWO1" || built.CountArgs[4] != "粤AG18312" {
|
||||||
t.Fatalf("batch count args = %#v", built.CountArgs)
|
t.Fatalf("batch count args = %#v", built.CountArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildVehicleRealtimeSQLBoundsCopiedPlateSet(t *testing.T) {
|
||||||
|
keywords := make([]string, 0, vehicleSearchKeywordLimit+10)
|
||||||
|
for index := 0; index < vehicleSearchKeywordLimit+10; index++ {
|
||||||
|
keywords = append(keywords, fmt.Sprintf("粤A%05d", index))
|
||||||
|
}
|
||||||
|
built := buildVehicleRealtimeSQL(url.Values{"keywords": {strings.Join(keywords, ",")}, "limit": {"10"}})
|
||||||
|
if len(built.Args) != vehicleSearchKeywordLimit*3+2 {
|
||||||
|
t.Fatalf("bounded batch args = %d, want %d", len(built.Args), vehicleSearchKeywordLimit*3+2)
|
||||||
|
}
|
||||||
|
if len(built.CountArgs) != vehicleSearchKeywordLimit*3 {
|
||||||
|
t.Fatalf("bounded batch count args = %d, want %d", len(built.CountArgs), vehicleSearchKeywordLimit*3)
|
||||||
|
}
|
||||||
|
if slices.Contains(built.Args, "粤A00100") {
|
||||||
|
t.Fatalf("batch query must ignore identities beyond the %d item boundary", vehicleSearchKeywordLimit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildVehicleRealtimeSQLFiltersServiceStatus(t *testing.T) {
|
func TestBuildVehicleRealtimeSQLFiltersServiceStatus(t *testing.T) {
|
||||||
query := url.Values{"serviceStatus": {"degraded"}, "limit": {"8"}}
|
query := url.Values{"serviceStatus": {"degraded"}, "limit": {"8"}}
|
||||||
built := buildVehicleRealtimeSQL(query)
|
built := buildVehicleRealtimeSQL(query)
|
||||||
|
|||||||
Reference in New Issue
Block a user