4 Commits

Author SHA1 Message Date
thekiwismarthome 91cbc6018c perf: reuse cached search engine instead of rebuilding on every query
search_products() and find_product_substitutes() were ignoring the
already-built self._search_engine and creating a brand new ProductSearch
instance on every call — serialising every product to dict and
re-initialising the fuzzy engine each time.

They now use self._search_engine directly. The engine is already kept
up-to-date by add_product / update_product / delete_product, so no
correctness change is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-11 17:45:43 +12:00
thekiwismarthome c55e90d74f chore: merge v2.6.2---Enhancements-and-Bug-Fixes into main
- Fix OFT search URL encoding for correct multi-word product queries
- Add EAN barcode code field to OFT search and barcode lookup responses
- Add sort_by=unique_scans_n for more relevant OFT search results
2026-08-11 04:11:14 +12:00
thekiwismarthome 408e360973 Merge pull request #21 from thekiwismarthome/main
need to catchup LOL
2026-08-10 16:38:42 +12:00
thekiwismarthome 98eb022573 fix: OFT search URL encoding and barcode field
- URL-encode search query so multi-word product names return correct results
- Add code (EAN barcode) to fields returned by both name search and
  barcode lookup — frontend can now auto-populate barcode on OFT apply
- Add sort_by=unique_scans_n to name search for more relevant ordering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-09 22:36:33 +12:00
2 changed files with 10 additions and 16 deletions
@@ -519,11 +519,7 @@ class ShoppingListStorage:
_LOGGER.warning("Search engine not initialized") _LOGGER.warning("Search engine not initialized")
return [] return []
# Convert products dict to format search engine expects results = self._search_engine.search(
products_dict = {pid: p.to_dict() for pid, p in self._products.items()}
search_engine = ProductSearch(products_dict)
results = search_engine.search(
query=query, query=query,
limit=limit, limit=limit,
exclude_allergens=exclude_allergens, exclude_allergens=exclude_allergens,
@@ -531,7 +527,6 @@ class ShoppingListStorage:
substitution_group=substitution_group, substitution_group=substitution_group,
) )
# Convert back to Product objects
return [self._products[r["id"]] for r in results if r["id"] in self._products] return [self._products[r["id"]] for r in results if r["id"] in self._products]
def find_product_substitutes(self, product_id: str, limit: int = 5) -> List[Product]: def find_product_substitutes(self, product_id: str, limit: int = 5) -> List[Product]:
@@ -547,10 +542,7 @@ class ShoppingListStorage:
if not self._search_engine: if not self._search_engine:
return [] return []
products_dict = {pid: p.to_dict() for pid, p in self._products.items()} results = self._search_engine.find_substitutes(product_id, limit)
search_engine = ProductSearch(products_dict)
results = search_engine.find_substitutes(product_id, limit)
return [self._products[r["id"]] for r in results if r["id"] in self._products] return [self._products[r["id"]] for r in results if r["id"] in self._products]
def get_product_suggestions(self, limit: int = 20) -> List[Product]: def get_product_suggestions(self, limit: int = 20) -> List[Product]:
@@ -4,6 +4,7 @@ import logging
import re import re
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from urllib.parse import quote_plus
import voluptuous as vol import voluptuous as vol
from aiohttp import ClientTimeout from aiohttp import ClientTimeout
@@ -1159,7 +1160,7 @@ async def websocket_off_fetch(
try: try:
if msg.get("barcode"): if msg.get("barcode"):
barcode = msg["barcode"] barcode = msg["barcode"]
fields = "product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price" fields = "code,product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price"
url = f"{base_url}/api/v2/product/{barcode}.json?fields={fields}" url = f"{base_url}/api/v2/product/{barcode}.json?fields={fields}"
async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp: async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp:
if not resp.ok: if not resp.ok:
@@ -1173,10 +1174,11 @@ async def websocket_off_fetch(
else: else:
query = msg.get("query", "") query = msg.get("query", "")
page_size = msg.get("page_size", 5) page_size = msg.get("page_size", 5)
fields = "product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price" fields = "code,product_name,categories_tags,image_front_thumb_url,image_front_url,image_url,price"
url = ( url = (
f"{base_url}/api/v2/search" f"{base_url}/api/v2/search"
f"?search_terms={query}&fields={fields}&page_size={page_size}" f"?search_terms={quote_plus(query)}&fields={fields}"
f"&page_size={page_size}&sort_by=unique_scans_n"
) )
async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp: async with session.get(url, timeout=ClientTimeout(total=10), headers=headers) as resp:
if not resp.ok: if not resp.ok: