Fix/search ranking - #226
Conversation
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds relevance-ranked catalog search: RegistryService.search now scores every spec from list_all() via the new ModelAlias.match_score instead of delegating to the adapter's own search, returning ScoredModelSpec pairs sorted by descending score with alphabetical name tie-breaks. Scoring weights name matches (exact > prefix > substring) above capability and alias matches, above category, above description-only mentions (issue #79), and is case-insensitive using Capability/Category .value for cross-version-stable formatting. A blank or whitespace-only query returns [], and queries matching nothing return [].
Worth a look
- RegistryService.search now returns ScoredModelSpec instead of ModelSpec —
src/modeldock/core/registry.py:27· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- search() return type changed from List[ModelSpec] to List[ScoredModelSpec] breaks callers —
src/modeldock/core/registry.py:27· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- search() no longer delegates to registry.search, ignoring adapter-specific search semantics —
src/modeldock/core/registry.py:27· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- RegistryService.search no longer delegates to RegistryPort.search —
src/modeldock/core/registry.py:38· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Combined lower-priority signals can outrank a name match —
src/modeldock/domain/model.py:363· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 403 functions depend on the 100 functions this change touches.
Health — this change adds coupling hotspots:
- new:
load_settings()— 13 callers, 6 callees - new:
RegistryService— 12 callers, 6 callees - new:
_manager()— 21 callers, 3 callees - new:
test_composite_versions_and_resolve_delegate()— 0 callers, 7 callees - new:
test_huggingface_specs_carry_provenance()— 0 callers, 7 callees - new:
test_cli_console_helpers()— 0 callers, 6 callees
Verification — 403 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 390 function(s) in the blast radius were not formally verified this run
· 6 more finding(s) on lines outside this diff (see the check run).
Code Review SuggestionsThanks for the PR — the core scoring logic is well-placed and the Critical1.
# current
raise ModelNotFoundError(f"Unknown model: {ref.name}")
# fix
raise ModelNotFoundError(ref.name)2. After this PR, 3. PR description has an unfilled placeholder Please confirm whether Important4. Alias scoring path is never exercised by the tests
ModelSpec(
name="meta-chat",
aliases=["llama"], # alias matches, name does not
category=Category.CHAT,
capabilities=[Capability.CHAT],
description="...",
)Then assert it appears in results when searching 5. Equality is a special case of substring containment, so the if q in category:
score += cls._CATEGORY_MATCH6. Name/alias mutual exclusivity should be documented The # Name/alias tier is exclusive (elif chain) — a spec earns at most one of
# these. Capability, category, and description are additive (separate if
# blocks) and can stack.
_NAME_EXACT = 100.0
...7.
Minor
|
Registry: search ranking by relevance (closes #79)
Problem
RegistryService.search()delegated straight to the adapter'ssearch(),which does an unweighted substring match across name/alias/capability/
category/description (
ModelAlias.matches_query). All matches areequally "relevant" regardless of where the query matched, so a model
whose description happens to mention the query ranks the same as an
exact name match.
Change
ModelAlias.match_score(spec, query) -> floatindomain/model.py,next to the existing
matches_query. Pure function, no I/O — scores aspec by where the query matched: name (exact > prefix > substring) >
alias > capability > category > description, using fixed weights.
ScoredModelSpec(pydantic model:spec: ModelSpec,score: float)in
domain/model.py, following the same pattern asModelInfo.RegistryService.search()now pulls the full catalog vialist_all(),scores every candidate with
ModelAlias.match_score, drops zero-scoreresults, and sorts by
(-score, name)for deterministic ordering. It nolonger calls the adapter's own
search().[](previouslymatches_querytreated an empty query as "match everything" — kept that method's
behavior unchanged for existing callers, but
search()now short-circuitsbefore scoring).
Breaking change
RegistryService.search()return type changes fromList[ModelSpec]toList[ScoredModelSpec]. Updated call sites:modeldock/__init__.py::search()—modeldock search— now prints the score alongside each resultWhy domain, not core?
The scoring rule is a pure business rule about what counts as a better
match, with no I/O — the same category as the existing
matches_query.Keeping it in
domain/(rather than duplicating matching logic incore/registry.py) keepscore/as pure composition, per AGENT.md.Testing
tests/unit/core/test_registry.pyusing a local_StaticRegistryfixture (deterministic 3-model catalog) covering:.spec/.score[]BundledRegistrytests untouched — that adapter's ownsearch()is unaffected by this change.ruff,mypy --strict,bandit,pytestlocally — all passing.Checklist
feature/search-rankingExceptionmd.search()example needs anote about scored results (check if you changed the SDK's return shape)