image search
This commit is contained in:
@@ -47,8 +47,13 @@ const (
|
||||
scanLookupTimeout = 5 * time.Second
|
||||
scanMaxLabelLen = 200
|
||||
scanCatalogueTopK = 15
|
||||
// Below this the best hit is not shown as a match at all.
|
||||
scanMinScore = 0.30
|
||||
// Below this the best hit is not shown as a match at all. A correct label
|
||||
// scores ~0.92 against its own product's vector and ~0.23 against an
|
||||
// unrelated one, so the floor sits in the empty middle of that split
|
||||
// rather than just above the unrelated band: at 0.30, "Paracetamol"
|
||||
// came back as "Paneer Makhni 500ml" (0.304) — a near-miss on an
|
||||
// unrelated row clears a floor set that close to the noise.
|
||||
scanMinScore = 0.50
|
||||
)
|
||||
|
||||
// ScanErrors the controller maps to statuses. Everything else is a 500.
|
||||
@@ -241,6 +246,14 @@ func (s *scanService) Confirm(ctx context.Context, req models.ScanConfirmRequest
|
||||
}
|
||||
resp.Store = chosen
|
||||
|
||||
// Distance on the store they tapped, for every outcome and not just the
|
||||
// out-of-stock one below: the app renders this store from the reply it
|
||||
// gets. The phone's fix is free to parse; the saved address costs a
|
||||
// query, so it is only reached for on the path that also ranks other
|
||||
// outlets. Without either, distanceKm leaves the -1 the repository set.
|
||||
lat, lng, hasPos := utils.ParseLatLng(string(req.Latitude), string(req.Longitude))
|
||||
chosen.DistanceKm = distanceKm(*chosen, lat, lng, hasPos)
|
||||
|
||||
row, err := s.repo.ProductAt(ctx, req.Tenantid, req.Locationid, req.Productid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -268,10 +281,10 @@ func (s *scanService) Confirm(ctx context.Context, req models.ScanConfirmRequest
|
||||
}
|
||||
|
||||
// The same product elsewhere, nearest first, with enough of it.
|
||||
lat, lng, hasPos := utils.ParseLatLng(string(req.Latitude), string(req.Longitude))
|
||||
if !hasPos {
|
||||
if hl, hg, ok, err := s.repo.CustomerHome(ctx, req.Customerid); err == nil && ok {
|
||||
lat, lng, hasPos = hl, hg, true
|
||||
chosen.DistanceKm = distanceKm(*chosen, lat, lng, hasPos)
|
||||
}
|
||||
}
|
||||
others := make([]models.ScanStore, 0, len(stores))
|
||||
|
||||
@@ -436,3 +436,68 @@ func TestCatalogueFamilyGroupsByVariantKeyThenName(t *testing.T) {
|
||||
t.Errorf("name match should join 1 and 2 only, got %+v", family)
|
||||
}
|
||||
}
|
||||
|
||||
// A vector neighbour that is merely not-quite-unrelated used to clear the old
|
||||
// 0.30 floor: in production "Paracetamol" came back as "Paneer Makhni 500ml"
|
||||
// on a 0.304 similarity. Correct labels land near 0.92, so nothing this weak
|
||||
// is a match.
|
||||
func TestLookupRefusesANearMissAboveTheOldFloor(t *testing.T) {
|
||||
repo := newLookupFixture()
|
||||
repo.vector = []repositories.CatalogueHit{{Brand: "amul", ID: 4, ProductName: "Paneer Makhni 500ml", Distance: 0.696}} // score 0.304
|
||||
repo.text = nil
|
||||
svc := NewScanService(repo, fakeEmbedder{vec: []float32{0.1}})
|
||||
|
||||
resp, err := svc.Lookup(context.Background(), models.ScanLookupRequest{Customerid: 5, Label: "Paracetamol"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Match != nil {
|
||||
t.Fatalf("0.304 is a near-miss, not a match; got %+v", resp.Match)
|
||||
}
|
||||
if resp.Available || len(resp.Stores) != 0 {
|
||||
t.Fatalf("nothing should be offered without a match; got %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm answers about the store the customer tapped, so that store carries a
|
||||
// distance on every outcome — not only on the out-of-stock path that ranks
|
||||
// alternatives. Absent any position it stays -1, the documented "unknown".
|
||||
func TestConfirmReportsDistanceToTheChosenStore(t *testing.T) {
|
||||
repo := newLookupFixture()
|
||||
repo.at = map[int]*repositories.StoreOptionRow{200: &repo.options[1]}
|
||||
svc := NewScanService(repo, nil)
|
||||
req := models.ScanConfirmRequest{Customerid: 5, Tenantid: 2, Locationid: 20, Productid: 200, Quantity: 4}
|
||||
|
||||
withPos := req
|
||||
withPos.Latitude, withPos.Longitude = "11.035", "77.035"
|
||||
resp, err := svc.Confirm(context.Background(), withPos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resp.Ok || resp.Store == nil {
|
||||
t.Fatalf("expected the in-stock answer, got %+v", resp)
|
||||
}
|
||||
if resp.Store.DistanceKm <= 0 {
|
||||
t.Fatalf("the phone sent a fix, so the tapped store has a distance; got %v", resp.Store.DistanceKm)
|
||||
}
|
||||
|
||||
// No fix from the phone, but a saved address on file.
|
||||
repo.homeLat, repo.homeLng, repo.homeOK = 11.035, 77.035, true
|
||||
resp, err = svc.Confirm(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Store == nil || resp.Store.DistanceKm != -1 {
|
||||
t.Fatalf("in stock is answered without reaching for the saved address; got %v", resp.Store)
|
||||
}
|
||||
|
||||
// Neither: unknown, and the app sorts it last.
|
||||
repo.homeOK = false
|
||||
resp, err = svc.Confirm(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Store == nil || resp.Store.DistanceKm != -1 {
|
||||
t.Fatalf("no position at all is -1; got %v", resp.Store)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user