BF-WAB / combined_api_search.py
SamiKoen's picture
fix: clean query for smart fallback to work properly
a584ddc
raw
history blame
26 kB
"""Combined API Search Module - Yeni tek API'yi kullanan arama sistemi"""
import requests
import xml.etree.ElementTree as ET
import os
import time
import logging
# Logger ayarla
logger = logging.getLogger(__name__)
def normalize_turkish(text):
"""Türkçe karakter normalizasyonu"""
if not text:
return ""
# Önce uppercase yap
text = text.upper()
# Türkçe karakterleri normalize et
replacements = {
'İ': 'I', 'I': 'I', 'ı': 'I', 'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I',
'Ş': 'S', 'ş': 'S', 'Ș': 'S', 'ș': 'S', 'Ś': 'S',
'Ğ': 'G', 'ğ': 'G', 'Ĝ': 'G', 'ĝ': 'G',
'Ü': 'U', 'ü': 'U', 'Û': 'U', 'û': 'U', 'Ù': 'U', 'Ú': 'U',
'Ö': 'O', 'ö': 'O', 'Ô': 'O', 'ô': 'O', 'Ò': 'O', 'Ó': 'O',
'Ç': 'C', 'ç': 'C', 'Ć': 'C', 'ć': 'C',
'Â': 'A', 'â': 'A', 'À': 'A', 'Á': 'A', 'Ä': 'A',
'Ê': 'E', 'ê': 'E', 'È': 'E', 'É': 'E', 'Ë': 'E'
}
for tr_char, en_char in replacements.items():
text = text.replace(tr_char, en_char)
# Birden fazla boşluğu tek boşluğa çevir
import re
text = re.sub(r'\s+', ' ', text)
return text.strip()
# Cache configuration
CACHE_DURATION = 3600 # 1 saat
cache = {
'combined_api': {'data': None, 'time': 0}
}
def get_combined_api_data():
"""Combined API'den veriyi çek ve cache'le"""
current_time = time.time()
# Cache kontrolü
if cache['combined_api']['data'] and (current_time - cache['combined_api']['time'] < CACHE_DURATION):
cache_age = (current_time - cache['combined_api']['time']) / 60
logger.info(f"📦 Combined API cache kullanılıyor (yaş: {cache_age:.1f} dakika)")
return cache['combined_api']['data']
try:
# Yeni combined API'yi çağır
url = 'https://video.trek-turkey.com/combined_trek_xml_api_v2.php'
response = requests.get(url, timeout=30)
if response.status_code == 200:
# XML parse et
root = ET.fromstring(response.content)
# Cache'e kaydet
cache['combined_api']['data'] = root
cache['combined_api']['time'] = current_time
# İstatistikleri logla
stats = root.find('stats')
if stats is not None:
matched = stats.find('matched_products')
match_rate = stats.find('match_rate')
if matched is not None and match_rate is not None:
logger.info(f"✅ Combined API: {matched.text} ürün eşleşti ({match_rate.text})")
return root
else:
logger.error(f"❌ Combined API HTTP Error: {response.status_code}")
return None
except Exception as e:
logger.error(f"❌ Combined API Hata: {e}")
return None
def search_products_combined_api(query):
"""Yeni combined API kullanarak ürün ara"""
try:
# Combined API verisini al
root = get_combined_api_data()
if not root:
return []
# Stop words - arama skorunu etkilememesi gereken kelimeler
stop_words = ['VAR', 'MI', 'MEVCUT', 'MU', 'STOK', 'STOGU', 'DURUMU', 'BULUNUYOR']
# Query'yi normalize et ve stop words'leri filtrele
query_words = normalize_turkish(query).upper().split()
query_words = [word for word in query_words if word not in stop_words]
query_normalized = ' '.join(query_words)
results = []
# Tüm ürünlerde ara
for product in root.findall('product'):
# Ürün bilgilerini al
stock_code = product.find('stock_code')
name = product.find('name')
label = product.find('label')
url = product.find('url')
image_url = product.find('image_url')
# Fiyat bilgileri
prices = product.find('prices')
regular_price = None
discounted_price = None
if prices is not None:
regular_price_elem = prices.find('regular_price')
discounted_price_elem = prices.find('discounted_price')
if regular_price_elem is not None:
regular_price = regular_price_elem.text
if discounted_price_elem is not None:
discounted_price = discounted_price_elem.text
# Kategori bilgileri
category = product.find('category')
main_category = None
sub_category = None
if category is not None:
main_cat_elem = category.find('main_category')
sub_cat_elem = category.find('sub_category')
if main_cat_elem is not None:
main_category = main_cat_elem.text
if sub_cat_elem is not None:
sub_category = sub_cat_elem.text
# Warehouse stok bilgileri
warehouse_stock = product.find('warehouse_stock')
stock_found = False
total_stock = 0
warehouses = []
if warehouse_stock is not None:
found_attr = warehouse_stock.get('found')
stock_found = found_attr == 'true'
if stock_found:
total_stock_elem = warehouse_stock.find('total_stock')
if total_stock_elem is not None:
total_stock = int(total_stock_elem.text)
# Depo bilgileri
warehouses_elem = warehouse_stock.find('warehouses')
if warehouses_elem is not None:
for wh in warehouses_elem.findall('warehouse'):
wh_name = wh.find('name')
wh_stock = wh.find('stock')
if wh_name is not None and wh_stock is not None:
warehouses.append({
'name': wh_name.text,
'stock': int(wh_stock.text)
})
# Arama kriteri kontrolü - Türkçe normalizasyon ile
searchable_text = ""
if name is not None:
searchable_text += normalize_turkish(name.text) + " "
if label is not None:
searchable_text += normalize_turkish(label.text) + " "
if stock_code is not None:
searchable_text += normalize_turkish(stock_code.text) + " "
if main_category is not None:
searchable_text += normalize_turkish(main_category) + " "
if sub_category is not None:
searchable_text += normalize_turkish(sub_category) + " "
# Geliştirilmiş arama eşleştirme - stop words filtrelenmiş query_words kullan
filtered_query_words = query_words # Zaten üstte filtrelenmiş
name_normalized = normalize_turkish(name.text.upper()) if name is not None else ""
# 1. Temel kelime eşleşmesi
basic_matches = sum(1 for word in filtered_query_words if word in searchable_text)
if basic_matches > 0:
# 2. Tam eşleşme bonusu - tüm query kelimeleri ürün adında
exact_bonus = 10 if all(word in name_normalized for word in filtered_query_words) else 0
# 3. Sıra bonus - kelimeler doğru sırada mı?
sequence_bonus = 0
if len(filtered_query_words) >= 2:
query_sequence = " ".join(filtered_query_words)
if query_sequence in name_normalized:
sequence_bonus = 5
# 4. Uzunluk penaltısı - çok uzun ürün adları düşük skor alsın
length_penalty = max(0, len(name_normalized.split()) - len(filtered_query_words)) * -1
# Toplam skor
total_score = basic_matches + exact_bonus + sequence_bonus + length_penalty
result = {
'stock_code': stock_code.text if stock_code is not None else '',
'name': name.text if name is not None else '',
'label': label.text if label is not None else '',
'url': url.text if url is not None else '',
'image_url': image_url.text if image_url is not None else '',
'regular_price': regular_price,
'discounted_price': discounted_price,
'main_category': main_category,
'sub_category': sub_category,
'stock_found': stock_found,
'total_stock': total_stock,
'warehouses': warehouses,
'match_score': total_score
}
results.append(result)
# Sonuçları match score'a göre sırala
results.sort(key=lambda x: x['match_score'], reverse=True)
logger.info(f"🔍 Combined API araması: '{query}' için {len(results)} sonuç")
return results[:10] # İlk 10 sonuç
except Exception as e:
logger.error(f"❌ Combined API arama hatası: {e}")
return []
def format_product_result_whatsapp(product_data, show_variants_info=False, all_results=None):
"""Ürün bilgisini WhatsApp için formatla"""
try:
name = product_data['name']
url = product_data['url']
image_url = product_data['image_url']
regular_price = product_data['regular_price']
discounted_price = product_data['discounted_price']
stock_found = product_data['stock_found']
total_stock = product_data['total_stock']
warehouses = product_data['warehouses']
result = []
# Ürün adı
result.append(f"🚲 **{name}**")
# Fiyat bilgisi - Original Trek fiyat yuvarlama algoritması (app.py'dan)
def format_price(price_str):
if not price_str:
return ""
try:
price_float = float(price_str)
# Original Trek fiyat yuvarlama algoritması
if price_float > 200000:
rounded = round(price_float / 5000) * 5000
elif price_float > 30000:
rounded = round(price_float / 1000) * 1000
elif price_float > 10000:
rounded = round(price_float / 100) * 100
else:
rounded = round(price_float / 10) * 10
# Türk Lirası formatı: 1.234,56
return f"{rounded:,.0f}".replace(",", ".")
except:
return price_str
if regular_price:
formatted_regular = format_price(regular_price)
if discounted_price and discounted_price != regular_price:
formatted_discounted = format_price(discounted_price)
result.append(f"💰 Fiyat: ~~{formatted_regular}~~ **{formatted_discounted} TL**")
result.append("🔥 İndirimli fiyat!")
else:
result.append(f"💰 Fiyat: {formatted_regular} TL")
# Stok durumu - Varyant detayları ile
if stock_found and total_stock > 0:
# Hangi mağazalarda var + varyant bilgisi (ana ürünse)
if warehouses:
# Ana ürün ise varyant detaylarını da göster
if show_variants_info and all_results and is_main_product(name):
result.append("📦 **Mevcut varyantlar:**")
# Bu ana ürünün varyantlarını bul
main_name_base = name.upper()
variant_details = {}
for product in all_results:
if (main_name_base in product['name'].upper() and
product['stock_found'] and
product['total_stock'] > 0 and
not is_main_product(product['name'])):
# Varyant isminden beden/renk çıkar
variant_name = product['name']
variant_part = variant_name.replace(main_name_base, '').strip()
if variant_part.startswith('GEN 3 (2026)'):
variant_part = variant_part.replace('GEN 3 (2026)', '').strip()
for wh in product['warehouses']:
if wh['stock'] > 0:
store_name = wh['name']
if store_name not in variant_details:
variant_details[store_name] = []
variant_details[store_name].append(variant_part)
# Mağaza bazında varyantları listele
for store, variants in variant_details.items():
result.append(f"• **{store}:** {', '.join(variants)}")
# Genel bilgi
result.append("📞 Diğer beden/renk teyidi için mağazaları arayın")
else:
# Normal stok bilgisi
available_stores = [wh['name'] for wh in warehouses if wh['stock'] > 0]
if available_stores:
result.append("📦 **Stokta mevcut mağazalar:**")
for store in available_stores:
result.append(f"• {store}")
else:
result.append("⚠️ **Stok durumu kontrol edilemiyor**")
result.append("📞 Güncel stok için mağazalarımızı arayın:")
result.append("• Caddebostan: 0543 934 0438")
result.append("• Alsancak: 0543 936 2335")
# Ürün linki
if url:
result.append(f"🔗 [Ürün Detayları]({url})")
# Ürün resmi (WhatsApp için) - Direkt URL WhatsApp'ta resmi gösterir
if image_url and image_url.startswith('https://'):
# WhatsApp'ta resmi direkt göstermek için sadece URL'i ver (link formatı değil)
result.append(f"📷 {image_url}")
return "\n".join(result)
except Exception as e:
logger.error(f"❌ WhatsApp format hatası: {e}")
return "Ürün bilgisi formatlanamadı"
def is_main_product(product_name):
"""Ürün adına bakarak ana ürün mü varyant mı kontrol et"""
name_upper = product_name.upper()
# Varyant göstergeleri - beden, renk, yıl belirticileri
variant_indicators = [
# Bedenler
' - XS', ' - S', ' - M', ' - L', ' - XL', ' - XXL',
' XS', ' S ', ' M ', ' L ', ' XL', ' XXL',
# Renkler
' - SİYAH', ' - MAVİ', ' - KIRMIZI', ' - YEŞİL', ' - BEYAZ', ' - MOR',
' SİYAH', ' MAVİ', ' KIRMIZI', ' YEŞİL', ' BEYAZ', ' MOR',
# İngilizce renkler
' - BLACK', ' - BLUE', ' - RED', ' - GREEN', ' - WHITE', ' - PURPLE',
' BLACK', ' BLUE', ' RED', ' GREEN', ' WHITE', ' PURPLE'
]
# Eğer bu göstergelerden biri varsa varyant
for indicator in variant_indicators:
if indicator in name_upper:
return False
return True
def get_warehouse_stock_combined_api(query):
"""Combined API kullanan warehouse search - eski fonksiyonla uyumlu"""
results = search_products_combined_api(query)
if not results:
return ["Ürün bulunamadı"]
# Akıllı model geçişi - eski model stokta yoksa yeni modeli öner
# Örn: "marlin 4" arandığında MARLIN 4 yoksa MARLIN 4 GEN 3'ü göster
query_normalized = normalize_turkish(query.upper())
# Query'yi temizle - "var mı", "stok", "fiyat" gibi ekleri çıkar
clean_query = query_normalized
for suffix in ['VAR MI', 'VAR MIYDI', 'STOK', 'STOKTA', 'FIYAT', 'FIYATI', 'KACA', 'NE KADAR']:
clean_query = clean_query.replace(' ' + suffix, '').replace(suffix + ' ', '').replace(suffix, '')
clean_query = clean_query.strip()
logger.info(f"🧹 Query temizlendi: '{query}' → '{clean_query}'")
# Genel ürün adı aranıyorsa (model numarası yok)
if not any(year in clean_query for year in ['GEN', '2024', '2025', '2026', '(20']):
# Stokta olan modelleri kontrol et
stocked_alternatives = []
for product in results:
name_normalized = normalize_turkish(product['name'].upper())
# Ana ürün adıyla eşleşiyor mu? (temizlenmiş query ile)
if clean_query in name_normalized:
# Ana ürün mü?
if is_main_product(product['name']):
# Stokta mı veya varyantları var mı?
if product['stock_found'] and product['total_stock'] > 0:
stocked_alternatives.append(product)
else:
# Ana ürün stokta yok, varyantları kontrol et
has_stocked_variants = False
for variant in results:
if (product['name'].upper() in variant['name'].upper() and
variant['stock_found'] and
variant['total_stock'] > 0 and
not is_main_product(variant['name'])):
has_stocked_variants = True
break
if has_stocked_variants:
stocked_alternatives.append(product)
# Eğer alternatifler varsa, en yenisini seç (genelde GEN 3, 2026 vs.)
if stocked_alternatives:
# Model yılı/versiyonu olanları öncelikle
def get_model_priority(product):
name = product['name'].upper()
if '2026' in name or 'GEN 3' in name:
return 3
elif '2025' in name or 'GEN 2' in name:
return 2
elif '2024' in name or 'GEN' in name:
return 1
else:
return 0
stocked_alternatives.sort(key=lambda x: get_model_priority(x), reverse=True)
# En yeni modeli öncelikle kullan
if stocked_alternatives:
results = [stocked_alternatives[0]] + [r for r in results if r != stocked_alternatives[0]]
# Önce ana ürünleri filtrele
main_products = []
variant_products = []
for product in results:
if is_main_product(product['name']):
main_products.append(product)
else:
variant_products.append(product)
# Ana ürünün stok bilgilerini varyantlarından topla
def enrich_main_product_with_variant_stock(main_product, all_results):
"""Ana ürün stoku yoksa varyant stoklarını topla - TAM EŞLEŞTİRME"""
if main_product['stock_found'] and main_product['total_stock'] > 0:
return main_product # Zaten stok bilgisi var
# Bu ana ürünün varyantlarını bul - TAM EŞLEŞTIRME
main_name = main_product['name'].upper()
related_variants = []
for product in all_results:
product_name_upper = product['name'].upper()
# TAM EŞLEŞTIRME: varyant ana ürünle TAM BAŞLAMALI
# Örn: "MARLIN 4 GEN 3 (2026) MOR - XS" → "MARLIN 4 GEN 3 (2026)" ile eşleşmeli
# "MARLIN 4 SİYAH - XS" → "MARLIN 4" ile eşleşmeli
variant_matches = False
if (product['stock_found'] and
product['total_stock'] > 0 and
not is_main_product(product['name'])):
# TAM ÜRÜN EŞLEŞTIRMESI - backup mantığıyla ters
# Ana ürün "MARLIN 4" ise
# Varyant "MARLIN 4 SİYAH - XS" EVET, "MARLIN 4 GEN 3 (...) MOR - XS" HAYIR
# Ana ürün tam adıyla başlamalı, fazla kelime olmamalı
if product_name_upper.startswith(main_name):
remaining_after_main = product_name_upper[len(main_name):].strip()
# Kalan kısımda GEN/2026 gibi model ayırıcıları varsa bu farklı ürün
model_separators = ['GEN 3', 'GEN', '(2026)', '(2025)', '2026', '2025']
has_model_separator = any(sep in remaining_after_main for sep in model_separators)
# Ana ürün model ayırıcısı içeriyorsa, varyant da içermeli
main_has_separators = any(sep in main_name for sep in model_separators)
# Ana ürün model separator içeriyorsa, varyant onun devamı olabilir
# Ana ürün model separator içermiyorsa, varyant da içermemeli
if main_has_separators:
# Ana ürün GEN 3 (2026) gibi → varyant da GEN 3'ün devamı olmalı
# Kalan kısımda model separator olmamalı (çünkü ana üründe zaten var)
if not has_model_separator:
# Varyant göstergeleri var mı?
if remaining_after_main and any(indicator in remaining_after_main for indicator in [' - ', ' MOR', ' SIYAH', ' MAVI', ' XS', ' S ', ' M ', ' L ', ' XL']):
variant_matches = True
else:
# Ana ürün model separator içermiyor → varyant da içermemeli
if not has_model_separator:
# Varyant göstergeleri var mı?
if remaining_after_main and any(indicator in remaining_after_main for indicator in [' - ', ' MOR', ' SIYAH', ' MAVI', ' XS', ' S ', ' M ', ' L ', ' XL']):
variant_matches = True
if variant_matches:
related_variants.append(product)
if related_variants:
# Varyant stoklarını topla
combined_warehouses = {}
total_stock = 0
for variant in related_variants:
total_stock += variant['total_stock']
for wh in variant['warehouses']:
wh_name = wh['name']
wh_stock = wh['stock']
if wh_name in combined_warehouses:
combined_warehouses[wh_name] += wh_stock
else:
combined_warehouses[wh_name] = wh_stock
# Ana ürünü stok bilgileriyle zenginleştir
main_product['stock_found'] = True
main_product['total_stock'] = total_stock
main_product['warehouses'] = [
{'name': name, 'stock': stock}
for name, stock in combined_warehouses.items()
if stock > 0
]
return main_product
# Öncelikle ana ürünleri kullan, Ana ürün yoksa varyantları göster
if main_products:
# Ana ürünleri varyant stoklarıyla zenginleştir
enriched_main_products = []
for main_product in main_products:
enriched = enrich_main_product_with_variant_stock(main_product, results)
enriched_main_products.append(enriched)
# Basit sorgu (örn: "marlin 4") için sadece en iyi ana ürün
query_words = normalize_turkish(query).upper().split()
product_query_words = [w for w in query_words if w not in ['FIYAT', 'STOK', 'VAR', 'MI', 'RENK', 'BEDEN']]
is_simple_product_query = len(product_query_words) <= 2
if is_simple_product_query:
selected_products = enriched_main_products[:1] # Sadece en iyi ana ürün
else:
selected_products = enriched_main_products[:3] # Birden fazla ana ürün
else:
# Ana ürün yoksa varyantları göster
selected_products = variant_products[:3]
formatted_results = []
for product in selected_products:
# Ana ürün ise varyant detaylarını göster
show_variants = is_main_product(product['name'])
formatted = format_product_result_whatsapp(product, show_variants_info=show_variants, all_results=results)
formatted_results.append(formatted)
return formatted_results
# Test fonksiyonu
if __name__ == "__main__":
# Test arama
logging.basicConfig(level=logging.INFO)
print("Combined API Test Başlıyor...")
test_queries = ["marlin", "trek tool", "bisiklet", "madone"]
for query in test_queries:
print(f"\n🔍 Test: '{query}'")
results = search_products_combined_api(query)
if results:
print(f"✅ {len(results)} sonuç bulundu")
for i, product in enumerate(results[:2]):
print(f"\n{i+1}. {product['name']}")
print(f" Fiyat: {product['regular_price']} TL")
print(f" Stok: {'✅' if product['stock_found'] else '❌'} ({product['total_stock']} adet)")
else:
print("❌ Sonuç bulunamadı")