Mirrors /opt/ai-apps/eh-search/ on the server, including the full FastAPI app (intent routing, FTS+fuzzy+substring hybrid, multi-source federation across vehicles + blog + brands + pages + static + tag bridge), SQL schema (Postgres materialized view with german_unaccent text search, pg_trgm for fuzzy), Dockerfile and compose. Sanitized the hardcoded password in sql/01_init.sql — replaced with REPLACE_ME_BEFORE_APPLYING placeholder since this repo is public. The eh-search service binds only on the private network (10.0.0.8:8200) and is reachable only via Pegasus nginx proxy at /api/search. Refs OP#1094 OP#1105 OP#1112 OP#1116 OP#1117
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""Exact lookups: Komm-Nr, DVN, VIN.
|
|
|
|
Loco-Soft Komm-Nr-Schema:
|
|
Type-Buchstabe (N/T/V/D/G/L) + DVN-Zahl (4-6 Stellen)
|
|
Beispiele: 'D9094' (Nissan Leaf), 'N8093' (Askoll XKP45), 'L9083' (Opel)
|
|
|
|
N = Neu, T = Tageszul., V = Vorfuehr,
|
|
D = Differenzbest., G = Gebraucht, L = Leihgabe
|
|
|
|
DVN allein ist eindeutig pro Fahrzeug, deshalb auch als Such-Einstieg möglich.
|
|
"""
|
|
import re
|
|
|
|
from app import db
|
|
from app.schemas import SearchResultItem
|
|
|
|
|
|
COLUMNS = """
|
|
vehicle_id, dvn, commission_number, vin, brand, model, title,
|
|
price::float8 as price, primary_image_id::text as primary_image_id,
|
|
directus_product_id, dealer_vehicle_type
|
|
"""
|
|
|
|
KOMM_NR_RE = re.compile(r"^([NTVDGLntvdgl])\s*(\d{4,6})$")
|
|
|
|
|
|
def normalize_komm_nr(raw: str) -> str | None:
|
|
"""'D 9094' / 'd9094' / ' D9094 ' -> 'D9094'. Returns None if not a valid pattern."""
|
|
if not raw:
|
|
return None
|
|
m = KOMM_NR_RE.match(raw.strip())
|
|
if not m:
|
|
return None
|
|
return m.group(1).upper() + m.group(2)
|
|
|
|
|
|
async def by_komm_nr(raw: str) -> list[SearchResultItem]:
|
|
"""Lookup by full Komm-Nr (Type-Letter + DVN). Eindeutig wenn vorhanden."""
|
|
normalized = normalize_komm_nr(raw)
|
|
if not normalized:
|
|
return []
|
|
sql = f"SELECT {COLUMNS} FROM search_vehicles WHERE commission_number = $1 LIMIT 5"
|
|
rows = await db.fetch(sql, normalized)
|
|
return [_row_to_item(r, matched_via="exact_komm") for r in rows]
|
|
|
|
|
|
async def by_dvn(number: str) -> list[SearchResultItem]:
|
|
"""Lookup by DVN allein (4-6 stellige Zahl ohne Type-Buchstabe).
|
|
DVN ist eindeutig pro Fahrzeug, also liefert das immer 0 oder 1 Treffer."""
|
|
try:
|
|
dvn_int = int(number)
|
|
except (ValueError, TypeError):
|
|
return []
|
|
sql = f"SELECT {COLUMNS} FROM search_vehicles WHERE dvn = $1 LIMIT 5"
|
|
rows = await db.fetch(sql, dvn_int)
|
|
return [_row_to_item(r, matched_via="exact_dvn") for r in rows]
|
|
|
|
|
|
async def by_vin(vin: str) -> list[SearchResultItem]:
|
|
sql = f"SELECT {COLUMNS} FROM search_vehicles WHERE vin = $1 LIMIT 5"
|
|
rows = await db.fetch(sql, vin)
|
|
return [_row_to_item(r, matched_via="exact_vin") for r in rows]
|
|
|
|
|
|
def _row_to_item(row, matched_via: str) -> SearchResultItem:
|
|
return SearchResultItem(
|
|
vehicle_id=row["vehicle_id"],
|
|
commission_number=row["commission_number"],
|
|
vin=row["vin"],
|
|
brand=row["brand"],
|
|
model=row["model"],
|
|
title=row["title"] or "",
|
|
price=row["price"],
|
|
primary_image_id=row["primary_image_id"],
|
|
directus_product_id=row["directus_product_id"],
|
|
score=1.0,
|
|
matched_via=matched_via,
|
|
)
|