Class: ActiveSanction::Query
- Inherits:
-
Object
- Object
- ActiveSanction::Query
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/query.rb
Overview
One screening request: what is known about the subject, and how the search is to be run.
query = ActiveSanction::Query.build(
name: "Bosco Ntaganda",
type: :individual,
date_of_birth: "1973",
countries: %w[CD],
sources: %i[ofac_sdn un_consolidated],
threshold: 75,
limit: 10
)
query.subject # => Scorer::Subject, folded once and held
query.threshold # => 75.0
Two kinds of field, and the line between them matters
Evidence -- the name, the type, the dates of birth, the nationalities,
the identifiers -- is what the scorer compares against a record, and it
belongs to Scorer::Subject, which this builds and holds. Search
options -- sources, threshold, limit -- decide which records are
looked at and how many come back, and they never touch a comparison.
Keeping them apart is what lets a stored decision be re-derived: the evidence says what was screened and the options say what the run was willing to return, and an audit needs both separately. It is also why Subject carries no threshold -- see the note at the end of that class.
Singular and plural spellings are both accepted
date_of_birth: and dates_of_birth:, country:, countries: and
nationalities:, identifier: and identifiers: all mean the same
thing. A caller with one date writes the singular and a caller with three
writes the plural, and neither should have to remember which this library
prefers. #to_h emits the plural, canonical spelling.
That resolution happens in .build and .from_h, which is what every
screening call goes through -- Matcher#screen builds one of these out of
whatever it was handed. .new takes the canonical names and nothing else,
in the manner of Scorer::Weights: one constructor states the shape and one
accepts what a caller wrote.
Defaults come from configuration, once, here
A query with no threshold: takes config.screening_threshold and one
with no limit: takes config.screening_limit, both read at construction
and then fixed. Nothing downstream reads a global: a Matcher screens the
numbers on the query it was given, so a configuration changed mid-batch
cannot produce a run that is half one threshold and half another.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
-
#limit ⇒ Integer
readonly
How many results to return, highest score first.
-
#sources ⇒ Array<Symbol>?
readonly
Which lists to screen against, or nil for every list the matcher holds.
-
#subject ⇒ Scorer::Subject
readonly
The evidence, folded once.
-
#threshold ⇒ Float
readonly
0..100.
Class Method Summary collapse
-
.build(value = nil, **overrides) ⇒ Query
Whatever a caller had, as a Query:.
-
.from_h(hash) ⇒ Query
Rebuilds a query from #to_h output, accepting string keys so one stored in an audit record survives the round-trip through JSON.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #dates_of_birth ⇒ Array<PartialDate>
-
#form ⇒ Normalizer::Form
The folded name every comparison runs against, and what a Matcher hands the index rather than the string it came from.
- #hash ⇒ Integer
- #identifiers ⇒ Array<Identifier>
-
#initialize(name:, type: nil, dates_of_birth: [], nationalities: [], identifiers: [], sources: nil, threshold: nil, limit: nil) ⇒ void
constructor
Only
nameis required; see Scorer::Subject on why that is the shape of the problem rather than a convenience. - #inspect ⇒ String
-
#merge(**overrides) ⇒ Query
This query with some fields replaced, which is how a batch applies one threshold to a list of names.
-
#name ⇒ String
The name as the caller wrote it, which is what a report quotes back.
- #nationalities ⇒ Array<String>
- #to_h ⇒ Hash{Symbol => T.untyped}
- #type ⇒ Symbol?
Constructor Details
#initialize(name:, type: nil, dates_of_birth: [], nationalities: [], identifiers: [], sources: nil, threshold: nil, limit: nil) ⇒ void
Only name is required; see Scorer::Subject on why that is the shape of
the problem rather than a convenience.
187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/active_sanction/query.rb', line 187 def initialize(name:, type: nil, dates_of_birth: [], nationalities: [], identifiers: [], sources: nil, threshold: nil, limit: nil) @subject = T.let( Scorer::Subject.new(name: name, type: type, dates_of_birth: dates_of_birth, nationalities: nationalities, identifiers: identifiers), Scorer::Subject ) @sources = T.let(sources!(sources), T.nilable(T::Array[Symbol])) @threshold = T.let(threshold!(threshold), Float) @limit = T.let(limit!(limit), Integer) freeze end |
Instance Attribute Details
#limit ⇒ Integer (readonly)
How many results to return, highest score first.
113 114 115 |
# File 'lib/active_sanction/query.rb', line 113 def limit @limit end |
#sources ⇒ Array<Symbol>? (readonly)
Which lists to screen against, or nil for every list the matcher holds. Naming one it does not hold is an error rather than a shorter answer -- see Matcher.
104 105 106 |
# File 'lib/active_sanction/query.rb', line 104 def sources @sources end |
#subject ⇒ Scorer::Subject (readonly)
The evidence, folded once. Every comparison in a screening run happens against this one object rather than against a name re-folded per candidate.
98 99 100 |
# File 'lib/active_sanction/query.rb', line 98 def subject @subject end |
#threshold ⇒ Float (readonly)
0..100. The lowest score worth reporting, and the number that decides what a screening call costs -- see Scorer.
109 110 111 |
# File 'lib/active_sanction/query.rb', line 109 def threshold @threshold end |
Class Method Details
.build(value = nil, **overrides) ⇒ Query
127 128 129 130 131 132 133 134 |
# File 'lib/active_sanction/query.rb', line 127 def build(value = nil, **overrides) case value when Query then overrides.empty? ? value : T.unsafe(value).merge(**overrides) when Hash then from_h(normalize(value).merge(normalize(overrides))) when nil then from_h(overrides) else from_h(normalize(overrides).merge(name: value)) end end |
.from_h(hash) ⇒ Query
Rebuilds a query from #to_h output, accepting string keys so one stored in an audit record survives the round-trip through JSON.
139 140 141 142 143 144 145 146 147 148 |
# File 'lib/active_sanction/query.rb', line 139 def from_h(hash) attributes = normalize(hash) unknown = attributes.keys - MEMBERS raise QueryError, "unknown Query attribute(s): #{unknown.join(", ")}" if unknown.any? # `new(**hash)` past a required keyword parameter is one of the few # things Sorbet cannot check statically. #initialize validates what # arrives, which is where a bad round-trip is caught. T.unsafe(self).new(**attributes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
232 233 234 235 236 |
# File 'lib/active_sanction/query.rb', line 232 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#dates_of_birth ⇒ Array<PartialDate>
213 |
# File 'lib/active_sanction/query.rb', line 213 def dates_of_birth = subject.dates_of_birth |
#form ⇒ Normalizer::Form
The folded name every comparison runs against, and what a Matcher hands the index rather than the string it came from.
224 |
# File 'lib/active_sanction/query.rb', line 224 def form = subject.form |
#hash ⇒ Integer
240 |
# File 'lib/active_sanction/query.rb', line 240 def hash = [self.class, to_h].hash |
#identifiers ⇒ Array<Identifier>
219 |
# File 'lib/active_sanction/query.rb', line 219 def identifiers = subject.identifiers |
#inspect ⇒ String
243 |
# File 'lib/active_sanction/query.rb', line 243 def inspect = "#<#{self.class} #{name.inspect} threshold=#{threshold} limit=#{limit}>" |
#merge(**overrides) ⇒ Query
This query with some fields replaced, which is how a batch applies one threshold to a list of names.
203 |
# File 'lib/active_sanction/query.rb', line 203 def merge(**overrides) = self.class.build(to_h, **overrides) |
#name ⇒ String
The name as the caller wrote it, which is what a report quotes back.
207 |
# File 'lib/active_sanction/query.rb', line 207 def name = subject.name |
#nationalities ⇒ Array<String>
216 |
# File 'lib/active_sanction/query.rb', line 216 def nationalities = subject.nationalities |
#to_h ⇒ Hash{Symbol => T.untyped}
227 228 229 |
# File 'lib/active_sanction/query.rb', line 227 def to_h subject.to_h.merge(sources: sources, threshold: threshold, limit: limit) end |
#type ⇒ Symbol?
210 |
# File 'lib/active_sanction/query.rb', line 210 def type = subject.type |