Skip to content

Commit a03f89b

Browse files
committed
feat: add fuzzy word-order independent item search
Implement multi-word search that finds items regardless of search term word order. Searching for "Blue Large" now correctly finds "Large Blue Plastic Widget". Implementation details: Backend (pos_next/api/items.py): - Dual search strategy for optimal performance and compatibility 1. Primary: MySQL fulltext boolean mode (MATCH AGAINST) with relevance scoring 2. Fallback: Pattern-based LIKE search with AND logic between words - Each word in the search query must match at least one field (item_code, item_name, or description) - Words can appear in any order across any field - Added `_build_item_base_conditions()` helper to centralize filter logic - Extracted field list to `ITEM_RESULT_FIELDS` constant for reusability - Search term deduplication prevents redundant LIKE predicates - Alphanumeric normalization handles special characters in search terms - Prefix matching for words ≥3 chars (+word*) and exact match for shorter tokens - Limit boolean terms to 8 for query performance Frontend (POS/src/utils/offline/items.js): - Updated `searchCachedItems()` for consistent word-order independence - Single-word queries use optimized IndexedDB index lookups (startsWithIgnoreCase) - Multi-word queries filter JavaScript-side using Array.every() for AND logic - Searches across item_code, item_name, description, and barcodes fields - Fetches 5x limit for multi-word to ensure adequate filtered results Technical notes: - Boolean search depends on `search_index` fulltext index on Item doctype - LIKE search provides compatibility when fulltext fails or is unavailable - Both online and offline search use consistent word-order-independent logic Fixes issue where multi-word searches required exact phrase order.
1 parent 75e1173 commit a03f89b

2 files changed

Lines changed: 318 additions & 198 deletions

File tree

POS/src/utils/offline/items.js

Lines changed: 106 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { db, getSetting, setSetting } from "./db"
1+
import { db, getSetting, setSetting } from "./db";
22

33
// Cache items in IndexedDB
44
export const cacheItems = async (items, priceList = null) => {
55
try {
6-
if (!items || items.length === 0) return
6+
if (!items || items.length === 0) return;
77

88
// Process items with barcodes
99
const processedItems = items.map((item) => ({
@@ -13,10 +13,10 @@ export const cacheItems = async (items, priceList = null) => {
1313
? item.item_barcode.map((b) => b.barcode).filter(Boolean)
1414
: [item.item_barcode]
1515
: [],
16-
}))
16+
}));
1717

1818
// Save to items table
19-
await db.items.bulkPut(processedItems)
19+
await db.items.bulkPut(processedItems);
2020

2121
// Save prices if price list is provided
2222
if (priceList) {
@@ -25,118 +25,137 @@ export const cacheItems = async (items, priceList = null) => {
2525
item_code: item.item_code,
2626
rate: item.rate || item.price_list_rate || 0,
2727
timestamp: Date.now(),
28-
}))
29-
await db.item_prices.bulkPut(prices)
28+
}));
29+
await db.item_prices.bulkPut(prices);
3030
}
3131

3232
// Update last sync time
33-
await setSetting("items_last_sync", Date.now())
33+
await setSetting("items_last_sync", Date.now());
3434

35-
console.log(`Cached ${items.length} items`)
36-
return true
35+
console.log(`Cached ${items.length} items`);
36+
return true;
3737
} catch (error) {
38-
console.error("Error caching items:", error)
39-
return false
38+
console.error("Error caching items:", error);
39+
return false;
4040
}
41-
}
41+
};
4242

4343
// Get cached items
4444
export const getCachedItems = async (limit = 100) => {
4545
try {
46-
const items = await db.items.limit(limit).toArray()
47-
return items
46+
const items = await db.items.limit(limit).toArray();
47+
return items;
4848
} catch (error) {
49-
console.error("Error getting cached items:", error)
50-
return []
49+
console.error("Error getting cached items:", error);
50+
return [];
5151
}
52-
}
52+
};
5353

54-
// Search cached items
54+
// Search cached items with fuzzy word-order independent matching
5555
export const searchCachedItems = async (searchTerm, limit = 50) => {
5656
try {
5757
if (!searchTerm) {
58-
return await db.items.limit(limit).toArray()
58+
return await db.items.limit(limit).toArray();
5959
}
6060

61-
const term = searchTerm.toLowerCase()
61+
const term = searchTerm.toLowerCase();
62+
const searchWords = term.split(/\s+/).filter((word) => word.length > 0);
63+
64+
// Single word search - use optimized index queries
65+
if (searchWords.length === 1) {
66+
const results = await db.items
67+
.where("item_code")
68+
.startsWithIgnoreCase(term)
69+
.or("item_name")
70+
.startsWithIgnoreCase(term)
71+
.or("barcodes")
72+
.equals(term)
73+
.limit(limit)
74+
.toArray();
75+
76+
return results;
77+
}
6278

63-
// Search by item code, name, or barcode
64-
const results = await db.items
65-
.where("item_code")
66-
.startsWithIgnoreCase(term)
67-
.or("item_name")
68-
.startsWithIgnoreCase(term)
69-
.or("barcodes")
70-
.equals(term)
71-
.limit(limit)
72-
.toArray()
79+
// Multi-word fuzzy search - fetch items and filter in JavaScript
80+
// Get a larger set for better multi-word matching
81+
const allItems = await db.items.limit(limit * 5).toArray();
82+
83+
// Filter items where ALL search words appear (order independent)
84+
const results = allItems.filter((item) => {
85+
const searchableText = `${item.item_code || ""} ${item.item_name || ""} ${
86+
item.description || ""
87+
} ${item.barcodes?.join(" ") || ""}`.toLowerCase();
88+
89+
// Check if all words are present in any order
90+
return searchWords.every((word) => searchableText.includes(word));
91+
});
7392

74-
return results
93+
return results.slice(0, limit);
7594
} catch (error) {
76-
console.error("Error searching cached items:", error)
77-
return []
95+
console.error("Error searching cached items:", error);
96+
return [];
7897
}
79-
}
98+
};
8099

81100
// Get item by barcode
82101
export const getItemByBarcode = async (barcode) => {
83102
try {
84-
const item = await db.items.where("barcodes").equals(barcode).first()
85-
return item
103+
const item = await db.items.where("barcodes").equals(barcode).first();
104+
return item;
86105
} catch (error) {
87-
console.error("Error getting item by barcode:", error)
88-
return null
106+
console.error("Error getting item by barcode:", error);
107+
return null;
89108
}
90-
}
109+
};
91110

92111
// Get item with price
93112
export const getItemWithPrice = async (itemCode, priceList) => {
94113
try {
95-
const item = await db.items.get(itemCode)
96-
if (!item) return null
114+
const item = await db.items.get(itemCode);
115+
if (!item) return null;
97116

98117
if (priceList) {
99118
const price = await db.item_prices.get({
100119
price_list: priceList,
101120
item_code: itemCode,
102-
})
121+
});
103122
if (price) {
104-
item.rate = price.rate
105-
item.price_list_rate = price.rate
123+
item.rate = price.rate;
124+
item.price_list_rate = price.rate;
106125
}
107126
}
108127

109-
return item
128+
return item;
110129
} catch (error) {
111-
console.error("Error getting item with price:", error)
112-
return null
130+
console.error("Error getting item with price:", error);
131+
return null;
113132
}
114-
}
133+
};
115134

116135
// Cache customers
117136
export const cacheCustomers = async (customers) => {
118137
try {
119-
if (!customers || customers.length === 0) return
138+
if (!customers || customers.length === 0) return;
120139

121-
await db.customers.bulkPut(customers)
122-
await setSetting("customers_last_sync", Date.now())
140+
await db.customers.bulkPut(customers);
141+
await setSetting("customers_last_sync", Date.now());
123142

124-
console.log(`Cached ${customers.length} customers`)
125-
return true
143+
console.log(`Cached ${customers.length} customers`);
144+
return true;
126145
} catch (error) {
127-
console.error("Error caching customers:", error)
128-
return false
146+
console.error("Error caching customers:", error);
147+
return false;
129148
}
130-
}
149+
};
131150

132151
// Search cached customers
133152
export const searchCachedCustomers = async (searchTerm, limit = 20) => {
134153
try {
135154
if (!searchTerm) {
136-
return await db.customers.limit(limit).toArray()
155+
return await db.customers.limit(limit).toArray();
137156
}
138157

139-
const term = searchTerm.toLowerCase()
158+
const term = searchTerm.toLowerCase();
140159

141160
const results = await db.customers
142161
.where("customer_name")
@@ -146,58 +165,57 @@ export const searchCachedCustomers = async (searchTerm, limit = 20) => {
146165
.or("email_id")
147166
.startsWithIgnoreCase(term)
148167
.limit(limit)
149-
.toArray()
168+
.toArray();
150169

151-
return results
170+
return results;
152171
} catch (error) {
153-
console.error("Error searching cached customers:", error)
154-
return []
172+
console.error("Error searching cached customers:", error);
173+
return [];
155174
}
156-
}
175+
};
157176

158177
// Get items last sync time
159178
export const getItemsLastSync = async () => {
160-
return await getSetting("items_last_sync", null)
161-
}
179+
return await getSetting("items_last_sync", null);
180+
};
162181

163182
// Get customers last sync time
164183
export const getCustomersLastSync = async () => {
165-
return await getSetting("customers_last_sync", null)
166-
}
184+
return await getSetting("customers_last_sync", null);
185+
};
167186

168187
// Check if cache is fresh (less than 24 hours old)
169188
export const isCacheFresh = async (type = "items") => {
170-
const lastSync =
171-
type === "items" ? await getItemsLastSync() : await getCustomersLastSync()
189+
const lastSync = type === "items" ? await getItemsLastSync() : await getCustomersLastSync();
172190

173-
if (!lastSync) return false
191+
if (!lastSync) return false;
174192

175-
const hoursSinceSync = (Date.now() - lastSync) / (1000 * 60 * 60)
176-
return hoursSinceSync < 24
177-
}
193+
const hoursSinceSync = (Date.now() - lastSync) / (1000 * 60 * 60);
194+
return hoursSinceSync < 24;
195+
};
178196

179197
// Clear cache
180198
export const clearItemsCache = async () => {
181199
try {
182-
await db.items.clear()
183-
await db.item_prices.clear()
184-
await setSetting("items_last_sync", null)
185-
console.log("Items cache cleared")
186-
return true
200+
await db.items.clear();
201+
await db.item_prices.clear();
202+
await setSetting("items_last_sync", null);
203+
console.log("Items cache cleared");
204+
return true;
187205
} catch (error) {
188-
console.error("Error clearing items cache:", error)
189-
return false
206+
console.error("Error clearing items cache:", error);
207+
return false;
190208
}
191-
}
209+
};
192210

193211
export const clearCustomersCache = async () => {
194212
try {
195-
await db.customers.clear()
196-
await setSetting("customers_last_sync", null)
197-
console.log("Customers cache cleared")
198-
return true
213+
await db.customers.clear();
214+
await setSetting("customers_last_sync", null);
215+
console.log("Customers cache cleared");
216+
return true;
199217
} catch (error) {
200-
console.error("Error clearing customers cache:", error)
201-
return false
218+
console.error("Error clearing customers cache:", error);
219+
return false;
202220
}
203-
}
221+
};

0 commit comments

Comments
 (0)