SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
25.8 KB · 465 lines python
Raw Blame History
1"""Search: Postgres FTS + trigram (+ pgvector when embeddings exist) and a deterministic natural-language → filter compiler (v2).23    "open models over 100B released this year"      → type model · openness open · params ≥ 1e11 · year_from = year_to = <this year>4    "cheapest models with 1M context"               → type model · context ≥ 1 000 000 · sort cheapest5    "reasoning models under $1/M tokens"            → type model · reasoning · max_output_price 1.06    "models that fit in 64GB"                       → type model · memory_gb 64 (→ ESTIMATED params bound at 4-bit)7    "Anthropic models released since 2025"          → type model · organization Anthropic (verified by the API against org aliases) · year_from 20258    "open vision models with Apache license"        → type model · openness open · modality image · license Apache-2.09    "papers introducing MoE models"                 → type paper · residual "introducing MoE"1011Every recognised fragment is listed in `compiled` (filter, label, value, source_span) so the UI can show "Compiled as …"; the rest of the12text is `residual` (used for full-text search) and its words are echoed in `unrecognised`."""13from __future__ import annotations1415import re16from dataclasses import dataclass, field17from datetime import UTC, datetime18from typing import Any1920from sqlalchemy.ext.asyncio import AsyncConnection2122from aiatlas.db import fetch_all23from aiatlas.ontology.licenses import normalize_license24from aiatlas.sdk.extract.numbers import parse_context_length, parse_param_count25from aiatlas.services import hardware_fit as hf2627TYPE_WORDS = {28    "model": ("model", "models", "llm", "llms", "language model", "language models"),29    "company": ("company", "companies", "startup", "startups", "lab", "labs", "organization", "organizations", "organisation"),30    "paper": ("paper", "papers", "research", "publication", "publications", "preprint", "preprints", "arxiv"),31    "provider": ("provider", "providers", "inference provider", "api provider", "inference providers"),32    "benchmark": ("benchmark", "benchmarks", "leaderboard", "leaderboards", "eval", "evals"),33    "hardware": ("gpu", "gpus", "chip", "chips", "hardware", "accelerator", "accelerators", "npu", "tpu"),34    "framework": ("framework", "frameworks", "library", "libraries", "runtime", "runtimes"),35    "dataset": ("dataset", "datasets"),36    "tool": ("tool", "tools", "agent", "agents", "app", "application"),37    "repository": ("repo", "repos", "repository", "repositories"),38}39MODALITY_WORDS = {"vision": "image", "multimodal": "image", "image": "image", "images": "image", "audio": "audio", "speech": "audio", "voice": "audio",40                  "video": "video", "embedding": "embedding", "embeddings": "embedding"}41LICENSE_WORDS = {"apache": "Apache-2.0", "apache 2.0": "Apache-2.0", "apache-2.0": "Apache-2.0", "mit": "MIT", "mit license": "MIT", "bsd": "BSD-3-Clause",42                 "cc-by": "CC-BY-4.0", "cc by": "CC-BY-4.0", "gpl": "GPL-3.0", "llama license": "Llama-3.1-Community", "gemma license": "Gemma-Terms"}43SORT_WORDS = {"cheapest": "cheapest", "cheap": "cheapest", "lowest price": "cheapest", "newest": "newest", "latest": "newest", "most recent": "newest",44              "recent": "newest", "largest": "largest", "biggest": "largest", "smallest": "smallest", "best": "best"}45STOP = {"the", "a", "an", "with", "and", "or", "of", "for", "that", "which", "in", "on", "to", "by", "from", "at", "my", "me", "i", "are", "is", "show", "find", "list", "all", "any"}46_SIZE = r"(\d+(?:\.\d+)?)\s*([bBmMtT])\b(?:\s*param(?:eter)?s?)?"474849@dataclass50class Query:51    text: str = ""52    entity_type: str | None = None53    openness: str | None = None54    year_from: int | None = None55    year_to: int | None = None56    params_min: int | None = None57    params_max: int | None = None58    context_min: int | None = None59    modalities: list[str] = field(default_factory=list)60    organization: str | None = None61    filters: dict[str, Any] = field(default_factory=dict)62    # v263    reasoning: bool | None = None64    max_input_price: float | None = None65    max_output_price: float | None = None66    memory_gb: float | None = None67    license_key: str | None = None68    commercial_use: bool | None = None69    days_back: int | None = None70    benchmark: str | None = None71    provider: str | None = None72    sort: str | None = None73    compiled: list[dict[str, Any]] = field(default_factory=list)74    residual: str = ""75    unrecognised: list[str] = field(default_factory=list)7677    def as_dict(self) -> dict[str, Any]:78        out = {k: v for k, v in self.__dict__.items() if v not in (None, "", [], {}) and k not in ("compiled", "residual", "unrecognised")}79        out["compiled"] = self.compiled80        out["residual"] = self.residual81        out["unrecognised"] = self.unrecognised82        out["filters"] = {**self.filters, "residual": self.residual}83        return out8485    @property86    def has_structure(self) -> bool:87        return any([self.entity_type, self.openness, self.year_from, self.year_to, self.params_min, self.params_max, self.context_min, self.modalities,88                    self.organization, self.reasoning, self.max_input_price, self.max_output_price, self.memory_gb, self.license_key, self.commercial_use,89                    self.days_back, self.benchmark, self.provider])909192class _Compiler:93    def __init__(self, text: str):94        self.text = text95        self.low = text.lower()96        self.consumed: list[tuple[int, int]] = []97        self.q = Query(text=text)9899    def take(self, m: re.Match[str], filter_: str, label: str, value: Any) -> None:100        self.consumed.append((m.start(), m.end()))101        self.q.compiled.append({"filter": filter_, "label": label, "value": value, "source_span": self.text[m.start():m.end()]})102103    def search(self, pattern: str) -> re.Match[str] | None:104        for m in re.finditer(pattern, self.low, flags=re.IGNORECASE):105            if not any(s <= m.start() < e or s < m.end() <= e for s, e in self.consumed):106                return m107        return None108109    def residual(self) -> str:110        chars = list(self.text)111        for s, e in self.consumed:112            for i in range(s, e):113                chars[i] = " "114        return " ".join("".join(chars).split())115116117def compile_query(q: str) -> Query:118    c = _Compiler(q.strip())119    out = c.q120    year_now = datetime.now(UTC).year121122    # --- explicit "benchmark <name>" / "provider <name>"123    m = c.search(r"\bbenchmark\s+([a-z0-9][\w.\- ]{1,40}?)(?=\s+(?:leaderboard|results|scores|models)\b|$)")124    if m:125        out.benchmark = m.group(1).strip()126        out.entity_type = "benchmark"127        c.take(m, "benchmark", "Benchmark", out.benchmark)128    m = c.search(r"\bprovider\s+([a-z0-9][\w.\- ]{1,40}?)(?=\s+(?:prices?|models)\b|$)")129    if m:130        out.provider = m.group(1).strip()131        out.entity_type = "provider"132        c.take(m, "provider", "Provider", out.provider)133134    # --- price bounds135    m = c.search(r"(?:under|below|cheaper than|less than|at most|<|≤)\s*\$?\s*(\d+(?:\.\d+)?)\s*(?:usd)?\s*(?:/|per)\s*(?:1\s*)?m(?:illion)?(?:\s*(?:tok(?:ens?)?|output(?:\s*tokens?)?|input(?:\s*tokens?)?))?")136    if m:137        which = "input" if "input" in m.group(0) else "output"138        val = float(m.group(1))139        if which == "input":140            out.max_input_price = val141        else:142            out.max_output_price = val143        c.take(m, f"max_{which}_price", f"Cheapest {which} price ≤ ${val:g} / 1M tokens", val)144    m = c.search(r"\$\s*(\d+(?:\.\d+)?)\s*(?:/|per)\s*(?:1\s*)?m(?:illion)?\b(?:\s*tok(?:ens?)?)?")145    if m and out.max_output_price is None and out.max_input_price is None:146        out.max_output_price = float(m.group(1))147        c.take(m, "max_output_price", f"Cheapest output price ≤ ${out.max_output_price:g} / 1M tokens", out.max_output_price)148149    # --- memory / hardware fit150    m = c.search(r"(?:fits?\s+(?:in|on)|runs?\s+on|run\s+locally\s+on|on\s+my)\s*(?:a\s+|an\s+)?(?:mac(?:book)?(?:\s+\w+)?\s+with\s+)?(\d+(?:\.\d+)?)\s*gb\b(?:\s*(?:of\s+)?(?:ram|memory|vram|unified memory))?")151    if not m:152        m = c.search(r"\b(\d+(?:\.\d+)?)\s*gb\s*(?:of\s+)?(?:ram|memory|vram)\b")153    if m:154        out.memory_gb = float(m.group(1))155        c.take(m, "memory_gb", f"Fits in {out.memory_gb:g} GB (estimated at 4-bit, 8K context)", out.memory_gb)156    elif c.search(r"\bon my mac\b"):157        m2 = c.search(r"\bon my mac\b")158        assert m2 is not None159        out.openness = "open"160        c.take(m2, "openness", "Runs locally → open weights", "open")161162    # --- context window163    m = c.search(r"(?:context(?:\s+window)?\s*(?:of\s+)?(?:>=|≥|>|over|above|at least|min(?:imum)?)?\s*(\d+(?:\.\d+)?\s*[kKmM])\b(?:\s*tokens?)?)")164    if not m:165        m = c.search(r"(?:with\s+)?(\d+(?:\.\d+)?\s*[kKmM])\b\s*(?:\+\s*)?(?:tokens?\s+)?(?:of\s+)?context(?:\s+window)?")166    if not m:167        m = c.search(r"\blong[\s-]context\b")168        if m:169            out.context_min = 128_000170            c.take(m, "context_min", "Context ≥ 128K tokens", 128_000)171            m = None172    if m:173        out.context_min = parse_context_length(m.group(1))174        c.take(m, "context_min", f"Context ≥ {out.context_min:,} tokens", out.context_min)175176    # --- parameter counts177    m = c.search(r"\bbetween\s+" + _SIZE + r"\s+and\s+" + _SIZE)178    if m:179        out.params_min = parse_param_count(m.group(1) + m.group(2) + " params")180        out.params_max = parse_param_count(m.group(3) + m.group(4) + " params")181        c.take(m, "params_range", f"Parameters between {m.group(1)}{m.group(2).upper()} and {m.group(3)}{m.group(4).upper()}", [out.params_min, out.params_max])182    m = c.search(r"(?:more than|over|above|at least|>=|≥|>|larger than|bigger than)\s*" + _SIZE)183    if m:184        out.params_min = parse_param_count(m.group(1) + m.group(2) + " params")185        c.take(m, "params_min", f"Parameters ≥ {m.group(1)}{m.group(2).upper()}", out.params_min)186    m = c.search(r"(?:less than|under|below|at most|<=|≤|<|smaller than)\s*" + _SIZE)187    if m:188        out.params_max = parse_param_count(m.group(1) + m.group(2) + " params")189        c.take(m, "params_max", f"Parameters ≤ {m.group(1)}{m.group(2).upper()}", out.params_max)190    m = c.search(r"\b" + _SIZE + r"\s*(?:models?|parameters?)\b")191    if m and out.params_min is None and out.params_max is None:192        n = parse_param_count(m.group(1) + m.group(2) + " params")193        if n:194            out.params_min, out.params_max = int(n * 0.85), int(n * 1.15)195            c.take(m, "params_about", f"About {m.group(1)}{m.group(2).upper()} parameters (±15%)", n)196197    # --- dates198    m = c.search(r"\b(?:(?:released|launched|published|announced)\s+)?(?:in\s+the\s+)?(?:last|past)\s+(\d+)\s+(day|week|month|year)s?\b")199    if m:200        n, unit = int(m.group(1)), m.group(2)201        out.days_back = n * {"day": 1, "week": 7, "month": 30, "year": 365}[unit]202        c.take(m, "days_back", f"Released in the last {n} {unit}{'s' if n > 1 else ''}", out.days_back)203    m = c.search(r"\b(?:released|launched|published|announced)?\s*(?:this|the current)\s+year\b")204    if m:205        out.year_from = out.year_to = year_now206        c.take(m, "year", f"Released in {year_now}", year_now)207    m = c.search(r"\b(?:released|launched|published|announced)?\s*(?:last|previous)\s+year\b")208    if m:209        out.year_from = out.year_to = year_now - 1210        c.take(m, "year", f"Released in {year_now - 1}", year_now - 1)211    m = c.search(r"\b(?:(?:released|launched|published|announced)\s+)?(?:since|after|from)\s+(20\d\d)\b")212    if m:213        out.year_from = int(m.group(1))214        c.take(m, "year_from", f"Released since {out.year_from}", out.year_from)215    m = c.search(r"\b(?:(?:released|launched|published|announced)\s+)?(?:before|until|up to)\s+(20\d\d)\b")216    if m:217        out.year_to = int(m.group(1))218        c.take(m, "year_to", f"Released before {out.year_to}", out.year_to)219    m = c.search(r"\b(?:(?:released|launched|published|announced)\s+)?in\s+(20\d\d)\b")220    if m:221        out.year_from = out.year_to = int(m.group(1))222        c.take(m, "year", f"Released in {m.group(1)}", int(m.group(1)))223    m = c.search(r"\b(20\d\d)\b")224    if m and out.year_from is None and out.year_to is None:225        out.year_from = out.year_to = int(m.group(1))226        c.take(m, "year", f"Released in {m.group(1)}", int(m.group(1)))227228    # --- licence229    m = c.search(r"\b(apache(?:[\s-]*2(?:\.0)?)?|mit|bsd|cc[\s-]by|gpl|llama|gemma)\s*(?:licen[cs]e[d]?)\b")230    if m:231        raw = m.group(1).strip()232        key = normalize_license(raw) or LICENSE_WORDS.get(raw.lower())233        if key:234            out.license_key = key235            c.take(m, "license", f"Licence {key}", key)236    m = c.search(r"\b(?:for\s+)?commercial(?:\s+use|ly usable|-use)?\b(?:\s+allowed|\s+ok)?")237    if m:238        out.commercial_use = True239        c.take(m, "commercial_use", "Licence allows commercial use", True)240241    # --- openness242    m = c.search(r"\b(open[- ]?(?:weights?|source|sourced)|open)\b(?=\s+(?:models?|llms?|vision|reasoning|coding|multimodal|\w+\s+models?)\b)") or c.search(r"\bopen[- ]?(?:weights?|source|sourced)\b")243    if m:244        out.openness = "open"245        c.take(m, "openness", "Open weights / open source", "open")246    else:247        m = c.search(r"\b(proprietary|closed(?:[- ]source)?|api[- ]only)\b")248        if m:249            out.openness = "proprietary"250            c.take(m, "openness", "Proprietary (API only)", "proprietary")251252    # --- reasoning253    m = c.search(r"\b(reasoning|thinking)\b(?=\s+(?:models?|llms?)\b)") or c.search(r"\bwith\s+(reasoning|thinking)\b")254    if m:255        out.reasoning = True256        c.take(m, "reasoning", "Reasoning / thinking models", True)257258    # --- modalities259    for w, mod in MODALITY_WORDS.items():260        m = c.search(rf"\b{w}\b")261        if m and mod not in out.modalities:262            out.modalities.append(mod)263            c.take(m, "modality", f"Modality: {mod}", mod)264265    # --- entity type: the earliest surviving type word wins ("papers introducing MoE models" → paper)266    if out.entity_type is None:267        best: tuple[int, str, re.Match[str]] | None = None268        for etype, words in TYPE_WORDS.items():269            for w in sorted(words, key=len, reverse=True):270                m = c.search(rf"\b{re.escape(w)}\b")271                if m and (best is None or m.start() < best[0]):272                    best = (m.start(), etype, m)273                    break274        if best:275            out.entity_type = best[1]276            c.take(best[2], "entity_type", f"Type: {best[1]}", best[1])277    if out.entity_type is None and (out.openness or out.params_min or out.params_max or out.context_min or out.reasoning or out.memory_gb or out.max_output_price or out.max_input_price):278        out.entity_type = "model"279        out.compiled.append({"filter": "entity_type", "label": "Type: model (implied)", "value": "model", "source_span": None})280281    # --- sort hints282    for w, s in sorted(SORT_WORDS.items(), key=lambda kv: -len(kv[0])):283        m = c.search(rf"\b{re.escape(w)}\b")284        if m:285            out.sort = s286            c.take(m, "sort", f"Sort: {s}", s)287            break288289    # --- organization: "by <Org>" / "from <Org>" / "<Org> models" (verified against the organization alias table by the API layer)290    m = c.search(r"\b(?:by|from)\s+([A-Za-z][\w.&-]+(?:\s+[A-Z][\w.&-]+)?)")291    if m and m.group(1).lower() not in STOP and m.group(1).lower() not in {w for ws in TYPE_WORDS.values() for w in ws}:292        out.organization = q[m.start(1):m.end(1)]293        c.take(m, "organization", f"Organization: {out.organization}", out.organization)294    else:295        # "<Org> models …" only at the very start of the query — a capitalised word in the middle ("introducing MoE models") is free text296        m = re.match(r"\s*(?:(?:all|the|show|list|find)\s+)?([A-Z][\w.&-]*(?:\s+[A-Z][\w.&-]*)?)\s+(?:models?|llms?|papers?|releases?)\b", q)297        if m and not any(s <= m.start(1) < e for s, e in c.consumed) and m.group(1).lower() not in STOP and m.group(1).lower() not in {w for ws in TYPE_WORDS.values() for w in ws} \298           and m.group(1).lower() not in ("open", "reasoning", "vision", "proprietary", "multimodal", "cheapest", "newest", "largest", "best", "new", "moe", "small", "large"):299            out.organization = m.group(1)300            c.consumed.append((m.start(1), m.end(1)))301            out.compiled.append({"filter": "organization", "label": f"Organization: {out.organization}", "value": out.organization, "source_span": m.group(1)})302303    residual = c.residual()304    residual = re.sub(r"\b(" + "|".join(sorted(STOP, key=len, reverse=True)) + r")\b", " ", residual, flags=re.IGNORECASE)305    residual = re.sub(r"[<>≤≥$]", " ", residual)306    out.residual = " ".join(residual.split())307    out.filters["residual"] = out.residual308    out.unrecognised = [w for w in re.findall(r"[\w.\-]+", out.residual) if len(w) > 1] if out.has_structure else []309    return out310311312def params_bound_from_memory(memory_gb: float, quant: str = "4bit") -> int:313    """ESTIMATED largest parameter count that fits `memory_gb` at 8K context (inverse of services.hardware_fit)."""314    kv = hf.KV_GB_PER_8K315    avail = max(0.0, memory_gb - hf.RESERVED_GB - kv)316    return int(avail * 1e9 / (hf.BYTES_PER_PARAM.get(quant, 0.5) * hf.OVERHEAD))317318319# ------------------------------------------------------------------------------------------------------------------ SQL320321322def _num(path: str) -> str:323    return f"(case when {path} ~ '^-?[0-9]+(\\.[0-9]+)?$' then ({path})::double precision end)"324325326def where_for(q: Query) -> tuple[list[str], dict[str, Any]]:327    """Filters shared by `search_entities` and the count query. Every numeric cast is regex-guarded."""328    where = ["e.merged_into is null"]329    params: dict[str, Any] = {}330    if q.entity_type:331        where.append("e.entity_type = :etype" if q.entity_type != "model" else "e.entity_type = 'model'")332        params["etype"] = q.entity_type333    if q.openness == "open":334        where.append("(e.attributes->>'openness' in ('open-weights','open-source','open') or e.attributes->>'weights_availability' = 'open')")335    elif q.openness == "proprietary":336        where.append("e.attributes->>'openness' in ('proprietary','closed')")337    date_col = "coalesce(e.attributes->>'release_date', e.attributes->>'published_at')"338    if q.year_from:339        where.append(f"left({date_col}, 4) >= :yf")340        params["yf"] = str(q.year_from)341    if q.year_to:342        where.append(f"left({date_col}, 4) <= :yt")343        params["yt"] = str(q.year_to)344    if q.days_back:345        where.append(f"({date_col} >= to_char((now() at time zone 'UTC') - make_interval(days => :days), 'YYYY-MM-DD') or e.first_seen_at >= now() - make_interval(days => :days))")346        params["days"] = int(q.days_back)347    pmax = q.params_max348    if q.memory_gb is not None:349        bound = params_bound_from_memory(q.memory_gb)350        pmax = min(pmax, bound) if pmax else bound351        where.append("e.attributes ? 'parameter_count'")352    if q.params_min:353        where.append(f"{_num('e.attributes->>' + chr(39) + 'parameter_count' + chr(39))} >= :pmin")354        params["pmin"] = float(q.params_min)355    if pmax:356        where.append(f"{_num('e.attributes->>' + chr(39) + 'parameter_count' + chr(39))} <= :pmax")357        params["pmax"] = float(pmax)358    if q.context_min:359        where.append(f"{_num('e.attributes->>' + chr(39) + 'context_length' + chr(39))} >= :cmin")360        params["cmin"] = float(q.context_min)361    for i, mod in enumerate(q.modalities):362        where.append(f"(e.attributes->'modalities' ? :mod{i} or e.attributes->'modalities_input' ? :mod{i} or e.attributes->'modalities_output' ? :mod{i}"363                     + (" or e.attributes->>'vision' = 'true'" if mod == "image" else "") + ")")364        params[f"mod{i}"] = mod365    if q.reasoning:366        where.append("e.attributes->>'reasoning' = 'true'")367    if q.max_output_price is not None:368        where.append("exists (select 1 from prices p where p.model_id = e.id and p.valid_to is null and p.output_per_mtok > 0 and p.output_per_mtok <= :maxout)")369        params["maxout"] = float(q.max_output_price)370    if q.max_input_price is not None:371        where.append("exists (select 1 from prices p where p.model_id = e.id and p.valid_to is null and p.input_per_mtok > 0 and p.input_per_mtok <= :maxin)")372        params["maxin"] = float(q.max_input_price)373    if q.license_key:374        where.append("(e.attributes->>'license_key' = :lic or lower(e.attributes->>'license') = any(cast(:lic_raw as text[])))")375        params["lic"] = q.license_key376        params["lic_raw"] = _raw_license_labels(q.license_key)377    if q.commercial_use:378        keys = _commercial_keys()379        where.append("(e.attributes->>'license_key' = any(cast(:ckeys as text[])) or lower(e.attributes->>'license') = any(cast(:craw as text[])))")380        params["ckeys"] = keys381        params["craw"] = [lbl for k in keys for lbl in _raw_license_labels(k)]382    if q.organization:383        where.append("exists (select 1 from entities o where o.id = e.organization_id and (o.canonical_name ilike :org or o.slug = :orgslug "384                     "or exists (select 1 from entity_aliases a where a.entity_id = o.id and a.alias ilike :org)))")385        params["org"] = q.organization386        params["orgslug"] = q.organization.lower().replace(" ", "-")387    return where, params388389390def _raw_license_labels(key: str) -> list[str]:391    from aiatlas.ontology.licenses import LICENSES392393    info = LICENSES.get(key)394    if not info:395        return [key.lower()]396    return sorted({key.lower(), *(a.lower() for a in info.aliases), *( [info.spdx.lower()] if info.spdx else [])})397398399def _commercial_keys() -> list[str]:400    from aiatlas.ontology.licenses import LICENSES401402    return sorted(k for k, v in LICENSES.items() if v.commercial_use is True and v.weights_downloadable)403404405SORT_SQL = {406    "cheapest": "(select min(p.output_per_mtok) from prices p where p.model_id = e.id and p.valid_to is null and p.output_per_mtok > 0) asc nulls last",407    "newest": "coalesce(e.attributes->>'release_date', e.attributes->>'published_at', '') desc",408    "largest": _num("e.attributes->>'parameter_count'") + " desc nulls last",409    "smallest": _num("e.attributes->>'parameter_count'") + " asc nulls last",410}411412413async def search_entities(conn: AsyncConnection, q: Query, *, limit: int = 30, offset: int = 0, embedding: list[float] | None = None) -> list[dict[str, Any]]:414    where, params = where_for(q)415    params.update({"limit": limit, "offset": offset})416    text = (q.residual or q.filters.get("residual") or "").strip() or ("" if q.has_structure else q.text)417    if q.benchmark and q.entity_type == "benchmark":418        text = q.benchmark419    if q.provider and q.entity_type == "provider":420        text = q.provider421    rank = "coalesce((e.quality->>'score')::float, 0) / 100.0"422    if text:423        params["q"] = text424        params["qlike"] = f"%{text}%"425        params["qprefix"] = " & ".join(f"{w}:*" for w in re.findall(r"\w+", text)[:8]) or text426        where.append("(e.search @@ to_tsquery('simple', :qprefix) or e.canonical_name ilike :qlike or e.canonical_name % :q "427                     "or exists (select 1 from entity_aliases a where a.entity_id = e.id and a.alias ilike :qlike))")428        rank = ("(ts_rank_cd(e.search, to_tsquery('simple', :qprefix)) * 2 + similarity(e.canonical_name, :q) * 3 "429                "+ case when e.canonical_name ilike :qlike then 1 else 0 end + coalesce((e.quality->>'score')::float, 0) / 200.0 "430                "+ case e.entity_type when 'model' then 0.3 when 'company' then 0.3 when 'provider' then 0.2 else 0 end)")431    if embedding is not None:432        params["vec"] = "[" + ",".join(f"{x:.6f}" for x in embedding) + "]"433        rank = f"({rank}) + coalesce(1 - (x.embedding <=> cast(:vec as vector)), 0) * 2"434        join = "left join entity_embeddings x on x.entity_id = e.id"435    else:436        join = ""437    order = SORT_SQL.get(q.sort or "", None)438    order_sql = f"{order}, rank desc" if order and q.entity_type in (None, "model") else "rank desc, e.updated_at desc"439    sql = f"""select e.id, e.entity_type, e.canonical_name, e.slug, left(e.description, 240) as description, e.status, e.attributes, e.quality,440                     o.canonical_name as organization_name, o.slug as organization_slug, {rank} as rank441              from entities e left join entities o on o.id = e.organization_id {join}442              where {' and '.join(where)} order by {order_sql} limit :limit offset :offset"""443    return await fetch_all(conn, sql, **params)444445446async def verify_organization(conn: AsyncConnection, name: str | None) -> dict[str, Any] | None:447    """The compiler only *proposes* an organization; it counts only when it matches an organization-like entity or one of its aliases."""448    if not name:449        return None450    rows = await fetch_all(conn, """select e.id, e.slug, e.canonical_name from entities e where e.entity_type in ('company','organization','lab','university') and e.merged_into is null451                                    and (e.canonical_name ilike :n or e.slug = :s or exists (select 1 from entity_aliases a where a.entity_id = e.id and a.alias ilike :n))452                                    order by length(e.canonical_name) limit 1""", n=name, s=name.lower().replace(" ", "-"))453    return rows[0] if rows else None454455456async def suggest(conn: AsyncConnection, prefix: str, *, limit: int = 8) -> list[dict[str, Any]]:457    return await fetch_all(conn, """select e.id, e.entity_type, e.canonical_name, e.slug, o.canonical_name as organization_name458                                    from entities e left join entities o on o.id = e.organization_id459                                    where e.merged_into is null and (e.canonical_name ilike :p or exists (select 1 from entity_aliases a where a.entity_id = e.id and a.alias ilike :p))460                                    order by case e.entity_type when 'model' then 0 when 'company' then 1 when 'provider' then 2 else 3 end,461                                             coalesce((e.quality->>'score')::float, 0) desc, length(e.canonical_name) limit :n""", p=f"{prefix}%", n=limit)462463464__all__ = ["Query", "compile_query", "params_bound_from_memory", "search_entities", "suggest", "verify_organization", "where_for"]465