Class: Vehicles::Dataset
- Inherits:
-
Object
- Object
- Vehicles::Dataset
- Defined in:
- lib/vehicles/dataset.rb
Overview
Loads the bundled snapshot once, builds in-memory indexes, and answers every query. No HTTP, no SQLite, no ActiveRecord on the read path — the first call builds the index, every call after is a hash lookup.
Instances are memoized per data path (see .load), so the JSON is parsed once per process.
Constant Summary collapse
- BUILTIN_ALIASES =
Built-in make aliases (normalized key => make slug). Common abbreviations and nicknames so whatever a user types tends to land. Diacritics and case are already handled by Vehicles.normalize, so "škoda"/"citroën" need no entry.
{ "vw" => "volkswagen", "vdub" => "volkswagen", "merc" => "mercedes-benz", "mercedes" => "mercedes-benz", "benz" => "mercedes-benz", "mb" => "mercedes-benz", "chevy" => "chevrolet", "beemer" => "bmw", "bimmer" => "bmw", "alfa" => "alfa-romeo", "landrover" => "land-rover", "range rover" => "land-rover", "rangerover" => "land-rover" # NOTE: no "vauxhall" alias — since 2026.07 the dataset ships Vauxhall # and Opel as separate makes (deliberately: separate GB/EU model names), # and an alias here would shadow the real Vauxhall records. }.freeze
Instance Attribute Summary collapse
-
#region ⇒ Object
readonly
Returns the value of attribute region.
-
#schema_version ⇒ Object
readonly
Returns the value of attribute schema_version.
-
#version ⇒ Object
readonly
Returns the value of attribute version.
Class Method Summary collapse
-
.load(path = Vehicles.data_path) ⇒ Object
Memoized per path so the bundled JSON is parsed only once per process.
-
.reset! ⇒ Object
Drop the cache (used by the test suite between runs).
Instance Method Summary collapse
-
#all_models ⇒ Object
Flat list of every Model (memoized + frozen — shared, so don't let callers mutate it).
-
#all_models_filtered(kind: nil, region: nil, rarity: nil, max_decile: nil) ⇒ Object
Every model matching optional kind/region/rarity filters, ranked by popularity.
-
#find_make(query) ⇒ Object
Resolve a make from a String/Symbol/Make via aliases, slug, or name.
-
#find_model(query) ⇒ Object
Resolve a free-text "make + model" string into one Model.
-
#initialize(raw) ⇒ Dataset
constructor
A new instance of Dataset.
-
#kinds ⇒ Object
Every kind present in the snapshot.
-
#makes(kind: nil, region: nil) ⇒ Object
All makes, optionally filtered by kind and/or continent.
-
#region?(region) ⇒ Boolean
Does this snapshot cover the given region? A "global" snapshot covers every region, so callers pinned to
region: :eukeep working as the dataset outgrows Europe. -
#search(query) ⇒ Object
Every model whose name (or full name) matches the query, ranked: exact name, then prefix, then substring, then full-name substring.
-
#top_models(kind: nil, country: nil, region: nil, limit: 20) ⇒ Object
Ranked models by popularity: global decile first, then breadth of availability as the tiebreaker, then name.
Constructor Details
#initialize(raw) ⇒ Dataset
Returns a new instance of Dataset.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/vehicles/dataset.rb', line 43 def initialize(raw) @version = raw["version"] @schema_version = raw["schema_version"] @region = raw["region"] # Name-alphabetical, diacritic-insensitive — the dataset file arrives # slug-sorted, which differs subtly ("Austin-Healey" vs "Austin Morris"). @makes = (raw["makes"] || []).map { |attrs| Make.new(attrs) } .sort_by { |m| Vehicles.normalize(m.name) } @by_slug = {} # raw slug => Make @index = {} # normalized (ASCII-folded) name/slug/alias => Make @exact = {} # downcased exact string => Make — for non-Latin aliases # (比亚迪, ヤマハ, 현대) that fold to "" under normalize @makes.each do |make| @by_slug[make.slug] = make index(make.name, make) index(make.slug, make) make.aliases.each do |a| index(a, make) @exact[a.downcase] ||= make end end end |
Instance Attribute Details
#region ⇒ Object (readonly)
Returns the value of attribute region.
41 42 43 |
# File 'lib/vehicles/dataset.rb', line 41 def region @region end |
#schema_version ⇒ Object (readonly)
Returns the value of attribute schema_version.
41 42 43 |
# File 'lib/vehicles/dataset.rb', line 41 def schema_version @schema_version end |
#version ⇒ Object (readonly)
Returns the value of attribute version.
41 42 43 |
# File 'lib/vehicles/dataset.rb', line 41 def version @version end |
Class Method Details
.load(path = Vehicles.data_path) ⇒ Object
Memoized per path so the bundled JSON is parsed only once per process.
31 32 33 |
# File 'lib/vehicles/dataset.rb', line 31 def load(path = Vehicles.data_path) (@instances ||= {})[path] ||= new(JSON.parse(File.read(path))) end |
.reset! ⇒ Object
Drop the cache (used by the test suite between runs).
36 37 38 |
# File 'lib/vehicles/dataset.rb', line 36 def reset! @instances = {} end |
Instance Method Details
#all_models ⇒ Object
Flat list of every Model (memoized + frozen — shared, so don't let callers
mutate it). Backs search.
142 143 144 |
# File 'lib/vehicles/dataset.rb', line 142 def all_models @all_models ||= @makes.flat_map(&:models).freeze end |
#all_models_filtered(kind: nil, region: nil, rarity: nil, max_decile: nil) ⇒ Object
Every model matching optional kind/region/rarity filters, ranked by popularity. The "give me a sensible slice" entry point.
167 168 169 170 171 172 173 174 |
# File 'lib/vehicles/dataset.rb', line 167 def all_models_filtered(kind: nil, region: nil, rarity: nil, max_decile: nil) list = all_models list = list.select { |m| m.kind == kind.to_sym } if kind list = list.select { |m| m.available_in_region?(region) } if region list = list.select { |m| m.rarity == rarity.to_sym } if rarity list = list.select { |m| m.global_decile && m.global_decile <= max_decile } if max_decile list end |
#find_make(query) ⇒ Object
Resolve a make from a String/Symbol/Make via aliases, slug, or name.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/vehicles/dataset.rb', line 81 def find_make(query) return query if query.is_a?(Make) q = Vehicles.normalize(query) # Non-Latin queries (比亚迪, 현대) fold to "" — try the exact index first. return @exact[query.to_s.strip.downcase] if q.empty? # 1. user-supplied aliases win if (canonical = Vehicles.configuration.aliases[q]) return @by_slug[Vehicles.slugify(canonical)] || @index[Vehicles.normalize(canonical)] end # 2. built-in aliases if (slug = BUILTIN_ALIASES[q]) return @by_slug[slug] end # 3. direct slug / normalized name / make alias @by_slug[q] || @index[q] end |
#find_model(query) ⇒ Object
Resolve a free-text "make + model" string into one Model. Tries the longest leading make prefix first ("land rover defender"), then the remainder as the model. Returns nil if nothing matches.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/vehicles/dataset.rb', line 104 def find_model(query) q = Vehicles.normalize(query) tokens = q.split return nil if tokens.empty? (tokens.length - 1).downto(1) do |i| make = find_make(tokens[0, i].join(" ")) next unless make model = make.model(tokens[i..].join(" ")) return model if model end nil end |
#kinds ⇒ Object
Every kind present in the snapshot. => [:bus, :car, :moped, ...]
147 148 149 |
# File 'lib/vehicles/dataset.rb', line 147 def kinds @kinds ||= @makes.flat_map(&:kinds).uniq.sort.freeze end |
#makes(kind: nil, region: nil) ⇒ Object
All makes, optionally filtered by kind and/or continent. region: is a
CONTINENT (:eu/:na/:as/:sa/:oc/:af) — a make matches if it's evidenced
there. (Legacy note: pre-1.0 region: meant "does the snapshot cover
this?"; on today's global snapshot both readings return European makes
for region: :eu, so existing callers are unaffected.) An unmapped
continent honestly returns [].
73 74 75 76 77 78 |
# File 'lib/vehicles/dataset.rb', line 73 def makes(kind: nil, region: nil) list = @makes list = list.select { |m| m.kinds.include?(kind.to_sym) } if kind list = list.select { |m| m.in_region?(region) } if region list end |
#region?(region) ⇒ Boolean
Does this snapshot cover the given region? A "global" snapshot covers
every region, so callers pinned to region: :eu keep working as the
dataset outgrows Europe.
179 180 181 |
# File 'lib/vehicles/dataset.rb', line 179 def region?(region) region_match?(region) end |
#search(query) ⇒ Object
Every model whose name (or full name) matches the query, ranked: exact name, then prefix, then substring, then full-name substring. Shorter names first.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/vehicles/dataset.rb', line 121 def search(query) q = Vehicles.normalize(query) return [] if q.empty? scored = [] all_models.each do |m| name_n = Vehicles.normalize(m.name) score = if name_n == q then 0 elsif name_n.start_with?(q) then 1 elsif name_n.include?(q) then 2 elsif Vehicles.normalize(m.full_name).include?(q) then 3 else next end scored << [score, m.name.length, m] end scored.sort_by { |score, len, _m| [score, len] }.map { |_s, _l, m| m } end |
#top_models(kind: nil, country: nil, region: nil, limit: 20) ⇒ Object
Ranked models by popularity: global decile first, then breadth of availability as the tiebreaker, then name. Unranked models (nil decile — catalog-only evidence) never appear: "unknown" must not outrank "known". top_models(kind: :car, country: :nl, limit: 10) top_models(kind: :motorcycle, region: :as, limit: 10) # by continent
156 157 158 159 160 161 162 163 |
# File 'lib/vehicles/dataset.rb', line 156 def top_models(kind: nil, country: nil, region: nil, limit: 20) c = country&.to_s&.downcase list = all_models.select(&:global_decile) list = list.select { |m| m.kind == kind.to_sym } if kind list = list.select { |m| m.availability.include?(c) } if c list = list.select { |m| m.available_in_region?(region) } if region list.sort_by { |m| [m.global_decile, -m.availability.size, m.name] }.first(limit) end |