mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 00:47:02 +01:00
fixed duplicate flavors and improved search bar relevance/fuzzy matching
This commit is contained in:
1 parent
42e6905db4
commit
c74524be17
4 files changed
+107
-18
No files matched your search
@@ -0,0 +1,44 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from api.models import Flavor, Rating
|
||||
from django.db.models import Count
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Identifies and merges duplicate flavors (same name and category)'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Find duplicates based on name and category
|
||||
duplicates = Flavor.objects.values('name', 'category').annotate(
|
||||
name_count=Count('id')
|
||||
).filter(name_count__gt=1)
|
||||
|
||||
self.stdout.write(f"Found {duplicates.count()} sets of duplicates.")
|
||||
|
||||
for dup in duplicates:
|
||||
name = dup['name']
|
||||
category_id = dup['category']
|
||||
|
||||
# Get all flavors in this set, ordered by having an external_id first, then by date
|
||||
flavor_set = Flavor.objects.filter(
|
||||
name=name,
|
||||
category_id=category_id
|
||||
).order_by('-external_id', 'created_at')
|
||||
|
||||
# Keep the first one, merge others into it
|
||||
keep = flavor_set[0]
|
||||
others = flavor_set[1:]
|
||||
|
||||
self.stdout.write(f"Merging {len(others)} duplicates into '{keep.name}' (ID: {keep.id}, Ext: {keep.external_id})")
|
||||
|
||||
for other in others:
|
||||
# Re-assign ratings
|
||||
Rating.objects.filter(flavor=other).update(flavor=keep)
|
||||
# Note: This might trigger unique constraint errors on Rating (user, flavor)
|
||||
# if the user rated both duplicates. We'll handle that if needed.
|
||||
|
||||
# Re-assign other related models if any (e.g. notifications, etc.)
|
||||
# In our case, Rating is the main one.
|
||||
|
||||
# Delete the duplicate
|
||||
other.delete()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Finished cleaning up duplicates."))
|
||||
@@ -78,7 +78,7 @@ class Command(BaseCommand):
|
||||
synced_external_ids = []
|
||||
|
||||
for p in products:
|
||||
title = p.get('title')
|
||||
title = p.get('title', '').strip()
|
||||
tags = p.get('tags', [])
|
||||
if isinstance(tags, str):
|
||||
tags = [t.strip() for t in tags.split(',')]
|
||||
@@ -119,23 +119,26 @@ class Command(BaseCommand):
|
||||
handle = p.get('handle')
|
||||
shop_url = f"https://weareholy.com/products/{handle}" if handle else None
|
||||
|
||||
# Find or create
|
||||
# Find or create - be very specific to avoid duplicates
|
||||
# 1. Try external_id
|
||||
flavor = Flavor.objects.filter(external_id=p['id']).first()
|
||||
|
||||
if not flavor:
|
||||
# Try to find by name AND category to avoid Milkshake/Iced Tea name collisions
|
||||
# 2. Try exact name match in category
|
||||
flavor = Flavor.objects.filter(name=title, category=category, is_legacy=False).first()
|
||||
|
||||
if not flavor:
|
||||
# 3. Try case-insensitive name match in category (safety for whitespace/case)
|
||||
flavor = Flavor.objects.filter(name__iexact=title, category=category, is_legacy=False).first()
|
||||
|
||||
if not flavor:
|
||||
flavor = Flavor(external_id=p['id'], name=title, category=category, is_legacy=False)
|
||||
created_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
|
||||
flavor.category = category
|
||||
flavor.description = description
|
||||
flavor.shop_url = shop_url
|
||||
flavor.is_available = is_available
|
||||
flavor.external_id = p['id'] # Ensure ID is set
|
||||
# Ensure external_id is updated if it was missing
|
||||
if not flavor.external_id:
|
||||
flavor.external_id = p['id']
|
||||
|
||||
if image_url:
|
||||
# Check if local file exists
|
||||
|
||||
+47
-9
@@ -106,24 +106,62 @@ class FlavorViewSet(viewsets.ModelViewSet):
|
||||
# Filter by remaining query words in name or description
|
||||
if remaining_query:
|
||||
words = remaining_query.split()
|
||||
# Construct a combined OR query for all words to be more permissive (fuzzy-ish)
|
||||
word_query = Q()
|
||||
for word in words:
|
||||
flavors = flavors.filter(
|
||||
Q(name__icontains=word) | Q(description__icontains=word)
|
||||
)
|
||||
if len(word) > 2: # Only filter by words longer than 2 chars to avoid noise
|
||||
word_query |= Q(name__icontains=word) | Q(description__icontains=word)
|
||||
|
||||
if word_query:
|
||||
flavors = flavors.filter(word_query)
|
||||
|
||||
# Limit results for performance
|
||||
results = []
|
||||
for f in flavors.order_by('name')[:15]:
|
||||
results.append({
|
||||
# Limit results and deduplicate in Python to be safe against DB inconsistencies
|
||||
results_list = []
|
||||
seen_keys = set()
|
||||
|
||||
# Priority sorting in Python
|
||||
search_lower = remaining_query.lower()
|
||||
|
||||
# Order by name, but we'll also try to prioritize exact matches in Python
|
||||
for f in flavors.order_by('name'):
|
||||
# Create a unique key for this flavor (name + category)
|
||||
key = f"{f.name.strip().lower()}|{f.category.slug}"
|
||||
if key in seen_keys:
|
||||
continue
|
||||
|
||||
seen_keys.add(key)
|
||||
|
||||
name_lower = f.name.lower()
|
||||
relevance = 0
|
||||
if name_lower == search_lower:
|
||||
relevance = 3
|
||||
elif name_lower.startswith(search_lower):
|
||||
relevance = 2
|
||||
elif search_lower in name_lower:
|
||||
relevance = 1
|
||||
|
||||
results_list.append({
|
||||
'id': f.id,
|
||||
'name': f.name,
|
||||
'type': 'flavor',
|
||||
'subtitle': f.category.name,
|
||||
'image_url': request.build_absolute_uri(f.image.url) if f.image else f.image_url,
|
||||
'slug': None
|
||||
'slug': None,
|
||||
'relevance': relevance
|
||||
})
|
||||
|
||||
return Response(results)
|
||||
# Sort by relevance desc, then name
|
||||
results_list.sort(key=lambda x: (-x['relevance'], x['name']))
|
||||
|
||||
# Remove relevance helper and limit to 15
|
||||
final_results = []
|
||||
for r in results_list:
|
||||
del r['relevance']
|
||||
final_results.append(r)
|
||||
if len(final_results) >= 15:
|
||||
break
|
||||
|
||||
return Response(final_results)
|
||||
|
||||
@action(detail=False, methods=['get'], permission_classes=[permissions.AllowAny])
|
||||
def top(self, request):
|
||||
|
||||
@@ -25,6 +25,10 @@ chmod -R 777 django_cache
|
||||
echo "Syncing flavors..."
|
||||
python manage.py sync_flavors
|
||||
|
||||
# Merge any duplicates that might have been created
|
||||
echo "Cleaning up duplicates..."
|
||||
python manage.py cleanup_duplicates
|
||||
|
||||
# Seed legacy flavors if needed
|
||||
echo "Seeding legacy flavors..."
|
||||
python manage.py seed_legacy_flavors
|
||||
|
||||
Reference in new issue
Block a user