davanstrien HF Staff commited on
Commit
d0a25b4
·
verified ·
1 Parent(s): 7528321

Fix result-set truncation, prefer canonical copies, add name channel

Browse files

Found via merve's hf-find traces (merve/hf-find-traces).

- Relevance path fetched n=k, so _knn's LIMIT 2k left near-duplicate collapse
no room to backfill and searches silently returned fewer than k (9 of 10 for
"RF-DETR segmentation", 5 of 10 with a param filter). Pool is now k*10 on the
relevance path; the explicit sort modes keep k*4 so widening the pool cannot
trade their relevance for popularity.
- Dup-collapse kept whichever copy scored a hair higher on cosine, which is
routinely a 0-download mirror. It now keeps the most-downloaded member of a
near-duplicate group, in the group's existing rank position.
- Summary embeddings cannot retrieve a repo by name: "iSAID" returned Icelandic
census records and chip-design netlists, while ariG23498/iSAID (whose summary
never says "iSAID") was unreachable. Added an id-matching channel, RRF-fused
at half weight so it rescues a miss without outvoting the vector ranking.
Tokens must look like identifiers and match at an id boundary -- a bare
substring put RASSAISAID/finance-deepseek-prompts above the real iSAID repos.
- Refresh likes/downloads at load from the pipeline's metadata snapshot. Corpus
rows freeze those counts at the moment a card was last summarised, so
dronefreak/UAVid-2020 read 0 downloads against 2454 live. Missing snapshot is
non-fatal.
- Added POPULARITY_WEIGHT, defaulting to OFF. It scores well on a canonical-repo
eval but a blind 3-judge A/B over 24 unseen queries split by domain: mainstream
14-2 for the prior, niche/historical/GLAM 3-5 against it. Rationale in-file.

Verified by test_ranking.py (14 fixture assertions, no corpus or model needed).

Files changed (1) hide show
  1. app.py +198 -17
app.py CHANGED
@@ -10,6 +10,7 @@ backend so query vectors stay aligned with the seed document embeddings.
10
  """
11
  import logging
12
  import os
 
13
  import time
14
  from contextlib import asynccontextmanager
15
  from typing import List, Optional
@@ -133,11 +134,39 @@ def load_table(cfg: str, table: str, has_param: bool):
133
  )
134
  )
135
  """)
 
136
  n = con.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
137
  STATE["counts"][table] = n
138
  logger.info(f"loaded {table} (from {cfg}): {n:,} rows @ {EMB_DIM}d")
139
 
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  @asynccontextmanager
142
  async def lifespan(app: FastAPI):
143
  t0 = time.time()
@@ -273,23 +302,41 @@ def _vec_literal(vec):
273
  DUP_SIM = 0.985 # near-identical cards (mirrors/re-uploads) collapse to the top-ranked copy
274
 
275
 
 
 
 
 
276
  def _knn(table, qvec, k, where_sql, cols, params=None, return_emb=False):
277
  """Top-k by cosine, with near-duplicate collapse.
278
 
279
- Overfetches 2x, then greedily drops rows whose embedding is ~identical
280
- (cos >= DUP_SIM) to a higher-ranked kept row — mirror repos otherwise eat
281
- adjacent result slots. Returns (cols..., sim) tuples; with return_emb=True,
282
- (cols..., emb, sim).
 
 
 
 
 
 
 
283
  """
284
  import numpy as np
285
  q = f"""SELECT {cols}, emb, array_cosine_similarity(emb, {_vec_literal(qvec)}) AS sim
286
- FROM {table} {where_sql} ORDER BY sim DESC LIMIT {int(k) * 2}"""
287
  rows = con.execute(q, params or []).fetchall()
288
  kept, kept_vecs = [], []
289
  for r in rows:
290
  v = np.asarray(r[-2], dtype=np.float32)
291
- if kept_vecs and float(np.max(np.stack(kept_vecs) @ v)) >= DUP_SIM:
292
- continue
 
 
 
 
 
 
 
293
  kept.append(r if return_emb else r[:-2] + (r[-1],))
294
  kept_vecs.append(v)
295
  if len(kept) >= int(k):
@@ -319,21 +366,88 @@ def _bm25_ids(kind: str, text: str, n: int) -> list:
319
  return [r[0] for r in rows]
320
 
321
 
322
- def _hybrid_fuse(table, kind, query, qvec, raw, k, where_sql, wparams):
323
- """RRF-fuse vector KNN rows with a BM25 channel over full card text.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- BM25-only hits are fetched from the in-memory table (with their true cosine
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  similarity) and must pass the same metadata filters as the vector channel.
 
 
327
  Returns rows in fused order, same tuple shape as _knn output.
328
  """
329
- bm_ids = _bm25_ids(kind, query, k * 2)
330
- if not bm_ids:
331
  return raw
332
  rrf: dict = {}
333
  for rank, rid in enumerate(r[0] for r in raw):
334
  rrf[rid] = rrf.get(rid, 0.0) + 1.0 / (60 + rank)
335
- for rank, rid in enumerate(bm_ids):
336
- rrf[rid] = rrf.get(rid, 0.0) + 1.0 / (60 + rank)
337
  order = sorted(rrf, key=rrf.get, reverse=True)
338
  known = {r[0]: r for r in raw}
339
  missing = [rid for rid in order if rid not in known][: k * 2]
@@ -349,6 +463,11 @@ def _hybrid_fuse(table, kind, query, qvec, raw, k, where_sql, wparams):
349
  return [known[rid] for rid in order if rid in known]
350
 
351
 
 
 
 
 
 
352
  async def _sort_results(rows, id_field, sort_by, k):
353
  """rows: list of dicts with keys id, similarity, summary, likes, downloads, [param_count]."""
354
  if sort_by == "trending":
@@ -367,10 +486,64 @@ async def _sort_results(rows, id_field, sort_by, k):
367
  rows.sort(key=lambda r: r.get("last_modified") or "", reverse=True)
368
  rows = rows[:k]
369
  else:
370
- rows = rows[:k]
371
  return rows
372
 
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  def _rows_to_dataset(raw):
375
  out = []
376
  for id_, summary, likes, downloads, param, task, license_, language, last_modified, sim in raw:
@@ -447,13 +620,17 @@ async def search_datasets(
447
  ):
448
  try:
449
  qvec = embed_query(f"Instruct: {DS_TASK}\nQuery:{query}")
450
- n = k * 4 if sort_by != "similarity" else (k * 2 if hybrid else k)
451
  where_sql, wparams = _where(min_likes, min_downloads,
452
  task=task, license=license, language=language,
453
  modified_after=modified_after)
454
  raw = _knn("dataset_cards", qvec, n, where_sql, COLS, wparams)
455
  if hybrid:
456
  raw = _hybrid_fuse("dataset_cards", "datasets", query, qvec, raw, k, where_sql, wparams)
 
 
 
 
457
  rows = await _sort_results(_rows_to_dataset(raw), "dataset", sort_by, k)
458
  return QueryResponse(results=[QueryResult(**{k2: v for k2, v in r.items() if k2 != "_id"}) for r in rows])
459
  except Exception as e:
@@ -512,13 +689,17 @@ async def search_models(
512
  try:
513
  use_param = min_param_count > 0 or max_param_count is not None
514
  qvec = embed_query(f"search_query: {query}")
515
- n = k * 4 if sort_by != "similarity" else (k * 2 if hybrid else k)
516
  where_sql, wparams = _where(min_likes, min_downloads, min_param_count, max_param_count, use_param,
517
  task=task, license=license, language=language,
518
  modified_after=modified_after)
519
  raw = _knn("model_cards", qvec, n, where_sql, COLS, wparams)
520
  if hybrid:
521
  raw = _hybrid_fuse("model_cards", "models", query, qvec, raw, k, where_sql, wparams)
 
 
 
 
522
  rows = await _sort_results(_rows_to_model(raw), "model", sort_by, k)
523
  return ModelQueryResponse(results=[ModelQueryResult(**{k2: v for k2, v in r.items() if k2 != "_id"}) for r in rows])
524
  except Exception as e:
 
10
  """
11
  import logging
12
  import os
13
+ import re
14
  import time
15
  from contextlib import asynccontextmanager
16
  from typing import List, Optional
 
134
  )
135
  )
136
  """)
137
+ _refresh_metadata(cfg, table)
138
  n = con.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
139
  STATE["counts"][table] = n
140
  logger.info(f"loaded {table} (from {cfg}): {n:,} rows @ {EMB_DIM}d")
141
 
142
 
143
+ def _refresh_metadata(cfg: str, table: str) -> None:
144
+ """Overwrite likes/downloads from the pipeline's metadata snapshot, if present.
145
+
146
+ Corpus rows carry the counts a repo had when its card was last summarised, and
147
+ cards rarely change — so a repo can sit at 0 downloads long after it is widely
148
+ used. Ranking now leans on those counts, so refresh them from the small
149
+ (id, downloads, likes) file the delta pipeline publishes each run. Absent file
150
+ or unreadable snapshot is not fatal: the corpus values still work, just stale.
151
+ """
152
+ try:
153
+ path = hf_hub_download(EMBEDDINGS_REPO, f"_state/metadata/{cfg}.parquet", repo_type="dataset")
154
+ except Exception as e:
155
+ logger.warning(f"{table}: no metadata snapshot ({e}); using corpus counts")
156
+ return
157
+ try:
158
+ n = con.execute(f"""
159
+ UPDATE {table} AS t
160
+ SET downloads = COALESCE(m.downloads, 0)::BIGINT,
161
+ likes = COALESCE(m.likes, 0)::BIGINT
162
+ FROM read_parquet('{path}') AS m
163
+ WHERE m.id = t.id
164
+ """).fetchone()
165
+ logger.info(f"{table}: refreshed metadata for {(n[0] if n else 0):,} rows")
166
+ except Exception as e:
167
+ logger.warning(f"{table}: metadata refresh failed ({e}); using corpus counts")
168
+
169
+
170
  @asynccontextmanager
171
  async def lifespan(app: FastAPI):
172
  t0 = time.time()
 
302
  DUP_SIM = 0.985 # near-identical cards (mirrors/re-uploads) collapse to the top-ranked copy
303
 
304
 
305
+ DUP_OVERFETCH = 6 # candidates per requested row, so dup-collapse can be backfilled
306
+ _DL_IDX = 3 # position of `downloads` within COLS (id, summary, likes, downloads, ...)
307
+
308
+
309
  def _knn(table, qvec, k, where_sql, cols, params=None, return_emb=False):
310
  """Top-k by cosine, with near-duplicate collapse.
311
 
312
+ Overfetches DUP_OVERFETCH x, then greedily drops rows whose embedding is
313
+ ~identical (cos >= DUP_SIM) to a higher-ranked kept row — mirror repos
314
+ otherwise eat adjacent result slots. When a duplicate is more downloaded than
315
+ the copy already kept, it replaces it in place: the group keeps its ranking
316
+ position but is represented by the canonical repo rather than whichever
317
+ mirror happened to score a hair higher.
318
+
319
+ The overfetch has to exceed the dup rate or collapse silently returns fewer
320
+ than k (popular model families are dominated by near-identical conversion
321
+ cards); the LIMIT is cheap because the full cosine scan dominates either way.
322
+ Returns (cols..., sim) tuples; with return_emb=True, (cols..., emb, sim).
323
  """
324
  import numpy as np
325
  q = f"""SELECT {cols}, emb, array_cosine_similarity(emb, {_vec_literal(qvec)}) AS sim
326
+ FROM {table} {where_sql} ORDER BY sim DESC LIMIT {int(k) * DUP_OVERFETCH}"""
327
  rows = con.execute(q, params or []).fetchall()
328
  kept, kept_vecs = [], []
329
  for r in rows:
330
  v = np.asarray(r[-2], dtype=np.float32)
331
+ if kept_vecs:
332
+ sims = np.stack(kept_vecs) @ v
333
+ j = int(np.argmax(sims))
334
+ if float(sims[j]) >= DUP_SIM:
335
+ # Same card, different repo: keep whichever copy people actually use.
336
+ if (r[_DL_IDX] or 0) > (kept[j][_DL_IDX] or 0):
337
+ kept[j] = r if return_emb else r[:-2] + (r[-1],)
338
+ kept_vecs[j] = v
339
+ continue
340
  kept.append(r if return_emb else r[:-2] + (r[-1],))
341
  kept_vecs.append(v)
342
  if len(kept) >= int(k):
 
366
  return [r[0] for r in rows]
367
 
368
 
369
+ # A query that is short and has no spaces is a repo name, not a description
370
+ # ("iSAID", "rtdetr"). Summary embeddings cannot retrieve those: the summary of
371
+ # ariG23498/iSAID never says "iSAID", and "iSAID" alone retrieves Icelandic
372
+ # census records via ID-ish subword tokens. Matching the id directly is the only
373
+ # channel that can find them.
374
+ _NAME_MAX_TOKENS = 3 # "iSAID instance segmentation" still carries a name token
375
+ _NAME_MIN_CHARS = 4 # shorter tokens ("ocr", "sam") match far too much
376
+ NAME_CHANNEL_WEIGHT = 0.5 # below the vector channel: rescue a miss, never dominate
377
+
378
+ # Hyphenated descriptors that pass the identifier test but name no repo.
379
+ _NAME_STOPWORDS = frozenset({"real-time", "state-of-the-art", "open-source", "fine-tuned"})
380
+
381
+
382
+ def _is_identifier_like(tok: str) -> bool:
383
+ """True for tokens shaped like a repo/model name rather than an English word.
384
+
385
+ Names carry a digit (yolov10), an internal capital (iSAID, RF-DETR), or a
386
+ separator (rf-detr). Ordinary description words ("herbarium", "element") carry
387
+ none of these, and matching those against ids returns download-ranked noise.
388
+ """
389
+ if tok.lower() in _NAME_STOPWORDS:
390
+ return False
391
+ return bool(
392
+ any(c.isdigit() for c in tok)
393
+ or any(c in "-_." for c in tok)
394
+ or re.search(r"[a-z][A-Z]|^[A-Z]{2,}$", tok)
395
+ )
396
+
397
+
398
+ def _name_tokens(query: str) -> list:
399
+ """Tokens from the query that plausibly name a repo rather than describe one."""
400
+ if len(query.strip()) > 60 or len(query.split()) > _NAME_MAX_TOKENS:
401
+ return []
402
+ return [
403
+ tok
404
+ for raw in re.split(r"[\s,]+", query.strip())
405
+ if len(tok := raw.strip("\"'()[]")) >= _NAME_MIN_CHARS and _is_identifier_like(tok)
406
+ ]
407
+
408
 
409
+ def _name_ids(table, tokens, n, where_sql, wparams):
410
+ """Top-n ids whose name contains any of these tokens, most-downloaded first.
411
+
412
+ The token must start at an id boundary (start, or after / _ . -). A bare
413
+ substring match puts `RASSAISAID/finance-deepseek-prompts` above the real
414
+ `ariG23498/iSAID` for the query "iSAID". The trailing side is deliberately left
415
+ open so name variants still match — "yolov10" has to find `jameslahm/yolov10x`.
416
+
417
+ Ordering by downloads (not cosine) is deliberate: this channel exists to find
418
+ the repo actually named, and RRF only uses the rank, so the canonical copy of
419
+ a name should lead its own channel.
420
+ """
421
+ if not tokens:
422
+ return []
423
+ cond = f"AND {where_sql[6:]}" if where_sql else ""
424
+ ors = " OR ".join("regexp_matches(id, ?, 'i')" for _ in tokens)
425
+ pats = [f"(^|[/_.-]){re.escape(t)}" for t in tokens]
426
+ rows = con.execute(
427
+ f"""SELECT id FROM {table}
428
+ WHERE ({ors}) {cond}
429
+ ORDER BY downloads DESC LIMIT {int(n)}""",
430
+ pats + list(wparams or []),
431
+ ).fetchall()
432
+ return [r[0] for r in rows]
433
+
434
+
435
+ def _fuse(table, qvec, raw, extra_ids, k, where_sql, wparams, weight=1.0):
436
+ """RRF-fuse the vector KNN rows with a second channel's id list.
437
+
438
+ Channel-only hits are fetched from the in-memory table (with their true cosine
439
  similarity) and must pass the same metadata filters as the vector channel.
440
+ `weight` scales the second channel's contribution, so a speculative channel can
441
+ rescue a miss without outvoting the vector ranking.
442
  Returns rows in fused order, same tuple shape as _knn output.
443
  """
444
+ if not extra_ids:
 
445
  return raw
446
  rrf: dict = {}
447
  for rank, rid in enumerate(r[0] for r in raw):
448
  rrf[rid] = rrf.get(rid, 0.0) + 1.0 / (60 + rank)
449
+ for rank, rid in enumerate(extra_ids):
450
+ rrf[rid] = rrf.get(rid, 0.0) + weight / (60 + rank)
451
  order = sorted(rrf, key=rrf.get, reverse=True)
452
  known = {r[0]: r for r in raw}
453
  missing = [rid for rid in order if rid not in known][: k * 2]
 
463
  return [known[rid] for rid in order if rid in known]
464
 
465
 
466
+ def _hybrid_fuse(table, kind, query, qvec, raw, k, where_sql, wparams):
467
+ """RRF-fuse vector KNN rows with a BM25 channel over full card text."""
468
+ return _fuse(table, qvec, raw, _bm25_ids(kind, query, k * 2), k, where_sql, wparams)
469
+
470
+
471
  async def _sort_results(rows, id_field, sort_by, k):
472
  """rows: list of dicts with keys id, similarity, summary, likes, downloads, [param_count]."""
473
  if sort_by == "trending":
 
486
  rows.sort(key=lambda r: r.get("last_modified") or "", reverse=True)
487
  rows = rows[:k]
488
  else:
489
+ rows = _apply_popularity_prior(rows)[:k]
490
  return rows
491
 
492
 
493
+ # Optional log-scaled download term, added to cosine to break ties toward the copy
494
+ # people actually use (a 0-download mirror otherwise outranks the canonical repo,
495
+ # since their cards read the same).
496
+ #
497
+ # OFF by default, deliberately. On hf-find's eval/queries.jsonl it looks like a
498
+ # clear win (mean recall 0.363 -> 0.435, zero-download slots 28.3% -> 6.2% at 0.08),
499
+ # but that eval's expected repos were chosen *because* they are canonical, so it
500
+ # cannot help but reward a popularity prior. A blind 3-judge A/B over 24 unseen
501
+ # queries split by domain instead:
502
+ #
503
+ # mainstream queries prior 14 - 2 baseline
504
+ # niche / historical / GLAM prior 3 - 5 baseline
505
+ #
506
+ # The mechanism: a 150k-download repo gets the full weight while a 50-download
507
+ # exact match gets ~a third of it, so any similarity gap under ~0.054 flips — and
508
+ # niche queries live in exactly that regime. Concretely, at 0.08 the query "OCR for
509
+ # historical printed text" ranks generic dots.ocr-1.5 above
510
+ # wjbmattingly/LightOnOCR-2-1B-cultural-heritage-english.
511
+ #
512
+ # So this is a domain trade, not a free improvement. Enable per-deployment with
513
+ # POPULARITY_WEIGHT=0.08 if the audience is mainstream model/dataset discovery.
514
+ POPULARITY_WEIGHT = float(os.getenv("POPULARITY_WEIGHT", "0"))
515
+
516
+
517
+ def _rerank_pool(k: int, sort_by: str) -> int:
518
+ """Candidate rows to pull before re-ranking down to k.
519
+
520
+ The relevance path re-ranks (popularity prior, optional name fusion) and gains
521
+ from a deeper pool: on eval/queries.jsonl mean recall is 0.363 at k*1, 0.399 at
522
+ k*4 and 0.435 at k*10, flat thereafter. The cosine scan touches every row
523
+ regardless, so a larger LIMIT is close to free.
524
+
525
+ The explicit sort modes keep the old k*4: they sort hard by likes/downloads, so
526
+ widening the pool would trade relevance for popularity rather than break ties.
527
+ """
528
+ return k * (10 if sort_by == "similarity" else 4)
529
+
530
+
531
+ def _apply_popularity_prior(rows):
532
+ """Re-rank by similarity + a log-scaled, per-result-set-normalised download term."""
533
+ if POPULARITY_WEIGHT <= 0 or not rows:
534
+ return rows
535
+ import math
536
+ mx = max((r.get("downloads") or 0) for r in rows)
537
+ if mx <= 0:
538
+ return rows
539
+ denom = math.log1p(mx)
540
+ return sorted(
541
+ rows,
542
+ key=lambda r: r["similarity"] + POPULARITY_WEIGHT * (math.log1p(r.get("downloads") or 0) / denom),
543
+ reverse=True,
544
+ )
545
+
546
+
547
  def _rows_to_dataset(raw):
548
  out = []
549
  for id_, summary, likes, downloads, param, task, license_, language, last_modified, sim in raw:
 
620
  ):
621
  try:
622
  qvec = embed_query(f"Instruct: {DS_TASK}\nQuery:{query}")
623
+ n = _rerank_pool(k, sort_by)
624
  where_sql, wparams = _where(min_likes, min_downloads,
625
  task=task, license=license, language=language,
626
  modified_after=modified_after)
627
  raw = _knn("dataset_cards", qvec, n, where_sql, COLS, wparams)
628
  if hybrid:
629
  raw = _hybrid_fuse("dataset_cards", "datasets", query, qvec, raw, k, where_sql, wparams)
630
+ elif (name_toks := _name_tokens(query)):
631
+ raw = _fuse("dataset_cards", qvec, raw,
632
+ _name_ids("dataset_cards", name_toks, k, where_sql, wparams),
633
+ k, where_sql, wparams, weight=NAME_CHANNEL_WEIGHT)
634
  rows = await _sort_results(_rows_to_dataset(raw), "dataset", sort_by, k)
635
  return QueryResponse(results=[QueryResult(**{k2: v for k2, v in r.items() if k2 != "_id"}) for r in rows])
636
  except Exception as e:
 
689
  try:
690
  use_param = min_param_count > 0 or max_param_count is not None
691
  qvec = embed_query(f"search_query: {query}")
692
+ n = _rerank_pool(k, sort_by)
693
  where_sql, wparams = _where(min_likes, min_downloads, min_param_count, max_param_count, use_param,
694
  task=task, license=license, language=language,
695
  modified_after=modified_after)
696
  raw = _knn("model_cards", qvec, n, where_sql, COLS, wparams)
697
  if hybrid:
698
  raw = _hybrid_fuse("model_cards", "models", query, qvec, raw, k, where_sql, wparams)
699
+ elif (name_toks := _name_tokens(query)):
700
+ raw = _fuse("model_cards", qvec, raw,
701
+ _name_ids("model_cards", name_toks, k, where_sql, wparams),
702
+ k, where_sql, wparams, weight=NAME_CHANNEL_WEIGHT)
703
  rows = await _sort_results(_rows_to_model(raw), "model", sort_by, k)
704
  return ModelQueryResponse(results=[ModelQueryResult(**{k2: v for k2, v in r.items() if k2 != "_id"}) for r in rows])
705
  except Exception as e: