Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
@@ -40,6 +40,139 @@ INDEX_SETTINGS = {
|
||||
'typoTolerance': {'enabled': True, 'minWordSizeForTypos': {'oneTypo': 4, 'twoTypos': 8}},
|
||||
}
|
||||
|
||||
# ─── Synonyms (A6) ──────────────────────────────────────────────────────────
|
||||
# Spanish/English aftermarket equivalents so that "brake pad" finds "BALATA"
|
||||
# and vice-versa. Built from a curated core plus the existing bilingual
|
||||
# taxonomy (services.translations.PART_TRANSLATIONS). Multi-word synonyms are
|
||||
# supported by Meilisearch.
|
||||
CURATED_SYNONYMS = [
|
||||
('balata', 'pastilla de freno', 'brake pad', 'brake pads'),
|
||||
('disco de freno', 'disco', 'brake disc', 'brake rotor'),
|
||||
('bujia', 'bujía', 'spark plug', 'spark plugs'),
|
||||
('clutch', 'embrague', 'cloch', 'clochs'),
|
||||
('faro', 'faro delantero', 'headlight', 'headlamp'),
|
||||
('mofle', 'mofle', 'silenciador', 'muffler'),
|
||||
('amortiguador', 'shock absorber', 'strut'),
|
||||
('rotula', 'rótula', 'ball joint'),
|
||||
('terminal', 'terminal de direccion', 'tie rod end'),
|
||||
('banda', 'banda serpentina', 'belt', 'serpentine belt'),
|
||||
('banda de distribucion', 'timing belt'),
|
||||
('bomba de agua', 'water pump'),
|
||||
('radiador', 'radiator'),
|
||||
('alternador', 'alternator'),
|
||||
('marcha', 'arrancador', 'starter', 'starter motor'),
|
||||
('bobina', 'bobina de encendido', 'ignition coil'),
|
||||
('bateria', 'batería', 'battery'),
|
||||
('filtro de aire', 'air filter'),
|
||||
('filtro de aceite', 'oil filter'),
|
||||
('filtro de gasolina', 'fuel filter'),
|
||||
('sensor', 'sensor'),
|
||||
('collarín', 'collarín de clutch', 'throw out bearing', 'release bearing'),
|
||||
('bomba de gasolina', 'fuel pump'),
|
||||
('foco', 'foco', 'bulb', 'lamp'),
|
||||
('cristal', 'cristal', 'glass', 'windshield'),
|
||||
('parabrisas', 'parabrisas', 'windshield'),
|
||||
('espejo', 'espejo lateral', 'mirror', 'side mirror'),
|
||||
('abanico', 'ventilador', 'cooling fan', 'fan'),
|
||||
('compresor', 'compresor de aire', 'a/c compressor', 'ac compressor'),
|
||||
('valvula', 'válvula', 'valve'),
|
||||
('junta', 'empaque', 'gasket'),
|
||||
('manguera', 'hose'),
|
||||
]
|
||||
|
||||
|
||||
def _strip_accents(text):
|
||||
if not text:
|
||||
return ''
|
||||
out = text
|
||||
for a, b in (('Á', 'A'), ('É', 'E'), ('Í', 'I'), ('Ó', 'O'), ('Ú', 'U'),
|
||||
('À', 'A'), ('È', 'E'), ('Ì', 'I'), ('Ò', 'O'), ('Ù', 'U'),
|
||||
('Ä', 'A'), ('Ë', 'E'), ('Ï', 'I'), ('Ö', 'O'), ('Ü', 'U'), ('Ñ', 'N')):
|
||||
out = out.replace(a, b)
|
||||
return out
|
||||
|
||||
|
||||
def _symmetrize(groups):
|
||||
"""{term: [other equivalent terms]} from groups of equivalent terms."""
|
||||
synonyms = {}
|
||||
for group in groups:
|
||||
terms = []
|
||||
for term in group:
|
||||
t = (term or '').strip().lower()
|
||||
if t and t not in terms:
|
||||
terms.append(t)
|
||||
for term in terms:
|
||||
others = [t for t in terms if t != term]
|
||||
synonyms.setdefault(term, [])
|
||||
for o in others:
|
||||
if o not in synonyms[term]:
|
||||
synonyms[term].append(o)
|
||||
# Accent-insensitive variant so "bujia" matches "bujía".
|
||||
plain = _strip_accents(term)
|
||||
if plain and plain != term:
|
||||
synonyms.setdefault(plain, [])
|
||||
for o in others:
|
||||
if o not in synonyms[plain]:
|
||||
synonyms[plain].append(o)
|
||||
return synonyms
|
||||
|
||||
|
||||
def build_synonyms(include_taxonomy=True):
|
||||
"""Build the synonym map for the search indexes.
|
||||
|
||||
Curated colloquial Mexican terms first, then the curated bilingual
|
||||
taxonomy in ``services.translations.PART_TRANSLATIONS`` (English → Spanish).
|
||||
"""
|
||||
groups = list(CURATED_SYNONYMS)
|
||||
if include_taxonomy:
|
||||
try:
|
||||
from services.translations import PART_TRANSLATIONS
|
||||
for en, es in PART_TRANSLATIONS.items():
|
||||
en, es = (en or '').strip(), (es or '').strip()
|
||||
if not en or not es or '/' in es:
|
||||
continue
|
||||
if len(en) > 40 or len(es) > 40:
|
||||
continue
|
||||
if en.lower() == es.lower():
|
||||
continue
|
||||
groups.append((en.lower(), es.lower()))
|
||||
except Exception:
|
||||
pass
|
||||
return _symmetrize(groups)
|
||||
|
||||
|
||||
_SYNONYMS_CACHE = None
|
||||
|
||||
|
||||
def get_synonyms():
|
||||
"""Cached synonym map (built once per process)."""
|
||||
global _SYNONYMS_CACHE
|
||||
if _SYNONYMS_CACHE is None:
|
||||
_SYNONYMS_CACHE = build_synonyms()
|
||||
return _SYNONYMS_CACHE
|
||||
|
||||
|
||||
# Per-index settings used by scripts/index_meilisearch.py. ``aliases`` holds
|
||||
# inventory_sku_aliases / interchange numbers for cross-reference search.
|
||||
INVENTORY_SEARCHABLE_ATTRIBUTES = ['part_number', 'name', 'brand', 'aliases']
|
||||
CATALOG_SEARCHABLE_ATTRIBUTES = ['sku', 'name', 'category', 'supplier_name', 'aliases']
|
||||
|
||||
|
||||
def inventory_index_settings():
|
||||
return {
|
||||
'searchableAttributes': list(INVENTORY_SEARCHABLE_ATTRIBUTES),
|
||||
'filterableAttributes': ['brand'],
|
||||
'synonyms': get_synonyms(),
|
||||
}
|
||||
|
||||
|
||||
def catalog_index_settings():
|
||||
return {
|
||||
'searchableAttributes': list(CATALOG_SEARCHABLE_ATTRIBUTES),
|
||||
'filterableAttributes': ['category', 'supplier_name'],
|
||||
'synonyms': get_synonyms(),
|
||||
}
|
||||
|
||||
_client = None
|
||||
_client_url = None
|
||||
|
||||
@@ -124,6 +257,51 @@ def search_parts(query, limit=50, offset=0):
|
||||
return None
|
||||
|
||||
|
||||
def is_enabled():
|
||||
"""True si MEILI_ENABLED está activo (se lee en cada llamada)."""
|
||||
return os.environ.get('MEILI_ENABLED', 'true').lower() == 'true'
|
||||
|
||||
|
||||
def search_index(index_uid, query, options=None):
|
||||
"""Búsqueda genérica en cualquier índice.
|
||||
|
||||
Returns:
|
||||
dict con la respuesta de Meilisearch, o None si Meilisearch está
|
||||
apagado, sin API key, o falla — el caller debe usar su fallback SQL.
|
||||
"""
|
||||
if not is_enabled():
|
||||
return None
|
||||
try:
|
||||
return get_client().index(index_uid).search(query, options or {})
|
||||
except MeilisearchApiError as e:
|
||||
# Índice inexistente (tenant sin reindexar) es esperado: silencio.
|
||||
if e.code != 'index_not_found':
|
||||
print(f"[meili_search] Search error on {index_uid}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[meili_search] Search error on {index_uid}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def inventory_index_name(db_name):
|
||||
"""Índice de inventario de un tenant (ver scripts/index_meilisearch.py)."""
|
||||
return f"nexus_inventory_{db_name}"
|
||||
|
||||
|
||||
def search_inventory(db_name, query, limit=50, offset=0):
|
||||
"""Busca en el índice de inventario de un tenant.
|
||||
|
||||
Returns:
|
||||
list de hits (posiblemente vacía) o None si Meilisearch no está
|
||||
disponible o el índice no existe → el caller debe usar SQL.
|
||||
"""
|
||||
res = search_index(inventory_index_name(db_name), query,
|
||||
{'limit': limit, 'offset': offset})
|
||||
if res is None:
|
||||
return None
|
||||
return res.get('hits', [])
|
||||
|
||||
|
||||
def add_part(part_doc):
|
||||
"""Add or update a single part document."""
|
||||
try:
|
||||
@@ -157,3 +335,56 @@ def clear_index():
|
||||
except Exception as e:
|
||||
print(f"[meili_search] Clear error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ─── A6: settings / synonym refresh ─────────────────────────────────────────
|
||||
|
||||
def _index_uids(prefix=None):
|
||||
"""UIDs of existing indexes (optionally filtered by prefix)."""
|
||||
client = get_client()
|
||||
try:
|
||||
result = client.get_indexes()
|
||||
except TypeError: # older meilisearch-python returns a dict
|
||||
result = client.get_indexes()
|
||||
if isinstance(result, dict):
|
||||
results = result.get('results', [])
|
||||
else:
|
||||
results = getattr(result, 'results', result)
|
||||
uids = []
|
||||
for item in results:
|
||||
uid = item.get('uid') if isinstance(item, dict) else getattr(item, 'uid', None)
|
||||
if uid and (prefix is None or uid.startswith(prefix)):
|
||||
uids.append(uid)
|
||||
return uids
|
||||
|
||||
|
||||
def apply_index_settings(index_uid, settings=None):
|
||||
"""Apply searchable attributes + synonyms to one index (no reindex)."""
|
||||
if settings is None:
|
||||
if index_uid == 'nexus_supplier_catalog':
|
||||
settings = catalog_index_settings()
|
||||
elif index_uid.startswith('nexus_inventory_'):
|
||||
settings = inventory_index_settings()
|
||||
else:
|
||||
settings = {'synonyms': get_synonyms()}
|
||||
task = get_client().index(index_uid).update_settings(settings)
|
||||
return task
|
||||
|
||||
|
||||
def apply_synonyms_to_all(prefixes=('nexus_inventory_', 'nexus_supplier_catalog')):
|
||||
"""Push the synonym map to every inventory/catalog index.
|
||||
|
||||
Updating ``synonyms`` does not require reindexing documents, so this is
|
||||
safe to run against a live Meilisearch. Returns {index_uid: ok}.
|
||||
"""
|
||||
results = {}
|
||||
for prefix in prefixes:
|
||||
uids = _index_uids(prefix)
|
||||
for uid in uids:
|
||||
try:
|
||||
apply_index_settings(uid)
|
||||
results[uid] = True
|
||||
except Exception as e:
|
||||
results[uid] = f"error: {e}"
|
||||
return results
|
||||
|
||||
|
||||
Reference in New Issue
Block a user