Skip to content

Commit beae973

Browse files
Merge pull request #138 from MITLibraries/term-fingerprints
Add automatic fingerprinting for Term records
2 parents eb9a734 + 051e7d7 commit beae973

13 files changed

Lines changed: 472 additions & 21 deletions

app/models/fingerprint.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# == Schema Information
4+
#
5+
# Table name: fingerprints
6+
#
7+
# id :integer not null, primary key
8+
# value :string
9+
# created_at :datetime not null
10+
# updated_at :datetime not null
11+
#
12+
class Fingerprint < ApplicationRecord
13+
has_many :terms, dependent: :nullify
14+
15+
validates :value, uniqueness: true
16+
17+
alias_attribute :fingerprint_value, :value
18+
19+
# This is similar to the SuggestedResource fingerprint method, with the exception that it also replaces &quot; with "
20+
# during its operation. This switch may also need to be added to the SuggestedResource method, at which point they can
21+
# be abstracted to a helper method.
22+
def self.calculate(phrase)
23+
modified = phrase
24+
modified = modified.strip
25+
modified = modified.downcase
26+
modified = modified.gsub('&quot;', '"') # This line does not exist in SuggestedResource implementation.
27+
modified = modified.gsub(/\p{P}|\p{S}/, '')
28+
modified = modified.to_ascii
29+
modified = modified.gsub(/\p{P}|\p{S}/, '')
30+
tokens = modified.split
31+
tokens = tokens.uniq
32+
tokens = tokens.sort
33+
tokens.join(' ')
34+
end
35+
end

app/models/term.rb

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,40 @@
77
#
88
# Table name: terms
99
#
10-
# id :integer not null, primary key
11-
# phrase :string
12-
# created_at :datetime not null
13-
# updated_at :datetime not null
14-
# flag :boolean
10+
# id :integer not null, primary key
11+
# phrase :string
12+
# created_at :datetime not null
13+
# updated_at :datetime not null
14+
# flag :boolean
15+
# fingerprint_id :integer
1516
#
1617
class Term < ApplicationRecord
1718
has_many :search_events, dependent: :destroy
1819
has_many :detections, dependent: :destroy
1920
has_many :categorizations, dependent: :destroy
2021
has_many :confirmations, dependent: :destroy
22+
belongs_to :fingerprint, optional: true
23+
24+
before_save :register_fingerprint
25+
after_destroy :check_fingerprint_count
2126

2227
scope :user_confirmed, -> { where.associated(:confirmations).distinct }
2328
scope :user_unconfirmed, -> { where.missing(:confirmations).distinct }
2429

30+
# The fingerprint method returns the constructed fingerprint field from the related Fingerprint record. In the
31+
# rare condition when no Fingerprint record exists, this method returns Nil.
32+
delegate :fingerprint_value, to: :fingerprint, allow_nil: true
33+
34+
# The cluster method returns an array of all Term records which share a fingerprint with the current term. The term
35+
# itself is not returned, so if a term has no related records, this method returns an empty array.
36+
#
37+
# @note In the rare case when a Term has no fingerprint, this method returns Nil.
38+
#
39+
# @return array
40+
def cluster
41+
fingerprint&.terms&.filter { |rel| rel != self }
42+
end
43+
2544
# The record_detections method is the one-stop method to call every Detector's record method that is defined within
2645
# the application.
2746
#
@@ -68,6 +87,22 @@ def calculate_categorizations
6887

6988
private
7089

90+
# register_fingerprint method gets called before a Term record is saved, ensuring that Terms should always have a
91+
# related Fingerprint method.
92+
def register_fingerprint
93+
new_record = {
94+
value: Fingerprint.calculate(phrase)
95+
}
96+
self.fingerprint = Fingerprint.find_or_create_by(new_record)
97+
end
98+
99+
# This is called during the after_destroy hook. If removing that term means that its fingerprint is now abandoned,
100+
# then we destroy the fingerprint too. In the rare case when a Term does not have a fingerprint, this method does not
101+
# cause problems because of the safe operators in the conditional.
102+
def check_fingerprint_count
103+
fingerprint.destroy if fingerprint&.terms&.count&.zero?
104+
end
105+
71106
# This method looks up all current detections for the given term, and assembles their confidence scores in a format
72107
# usable by the calculate_categorizations method. It exists to transform data like:
73108
# [{3=>0.91}, {1=>0.1}] and [{3=>0.95}]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class CreateFingerprints < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :fingerprints do |t|
4+
t.string :value, index: { unique: true, name: 'unique_fingerprint' }
5+
t.timestamps
6+
end
7+
end
8+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class AddFingerprintToTerms < ActiveRecord::Migration[7.1]
2+
def up
3+
add_reference :terms, :fingerprint, foreign_key: true
4+
end
5+
6+
def down
7+
remove_reference :terms, :fingerprint, foreign_key: true
8+
end
9+
end

db/schema.rb

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/classes.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ classDiagram
1818
direction LR
1919
2020
Term --> SearchEvent : has many
21+
Fingerprint --> Term : has many
2122
2223
Term "1" --> "1..*" Detection
2324
Term "1" --> "0..*" Categorization
@@ -41,8 +42,15 @@ classDiagram
4142
class Term
4243
Term: id
4344
Term: +String phrase
44-
Term: combinedScores()
45-
Term: recordDetections()
45+
Term: calculate_categorizations()
46+
Term: calculate_confidence(values)
47+
Term: cluster()
48+
Term: fingerprint()
49+
Term: record_detections()
50+
51+
class Fingerprint
52+
Fingerprint: id
53+
Fingerprint: +String fingerprint
4654
4755
class SearchEvent
4856
SearchEvent: +Integer id
@@ -111,17 +119,17 @@ classDiagram
111119
112120
namespace SearchActivity{
113121
class Term
122+
class Fingerprint
114123
class SearchEvent
115124
}
116125
117126
namespace KnowledgeGraph{
118-
class Detectors
127+
class Detector
119128
class DetectorCategory
120129
class Category
121130
}
122131
123132
namespace Detectors {
124-
class Detector
125133
class DetectorJournal["Detector::Journal"]
126134
class DetectorLcsh["Detector::Lcsh"]
127135
class DetectorStandardIdentifier["Detector::StandardIdentifiers"]
@@ -136,6 +144,7 @@ classDiagram
136144
137145
style SearchEvent fill:#000,stroke:#66c2a5,color:#66c2a5,stroke-width:4px;
138146
style Term fill:#000,stroke:#66c2a5,color:#66c2a5,stroke-width:4px;
147+
style Fingerprint fill:#000,stroke:#66c2a5,color:#66c2a5,stroke-width:4px;
139148
140149
style Category fill:#000,stroke:#fc8d62,color:#fc8d62
141150
style DetectorCategory fill:#000,stroke:#fc8d62,color:#fc8d62

lib/tasks/fingerprints.rake

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
namespace :fingerprints do
4+
# generate will create (or re-create) fingerprints for all existing Terms.
5+
desc 'Generate fingerprints for all terms'
6+
task generate: :environment do |_task|
7+
Rails.logger.info("Generating fingerprints for all #{Term.count} terms")
8+
9+
Term.find_each.with_index do |t, index|
10+
t.save
11+
Rails.logger.info("Processed #{index}") if index == (index / 1000) * 1000
12+
end
13+
end
14+
end

test/fixtures/fingerprints.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# == Schema Information
2+
#
3+
# Table name: fingerprints
4+
#
5+
# id :integer not null, primary key
6+
# value :string
7+
# created_at :datetime not null
8+
# updated_at :datetime not null
9+
#
10+
cool:
11+
value: cool search super
12+
13+
hi:
14+
value: hello world
15+
16+
pmid_38908367:
17+
value: '2024 38908367 activation aging al and cell dna et hallmarks hs methylation multiple pmid shim targets tert'
18+
19+
lcsh:
20+
value: 'geology massachusetts'
21+
22+
issn_1075_8623:
23+
value: '10758623'
24+
25+
doi:
26+
value: '101016jphysio201012004'
27+
28+
isbn_9781319145446:
29+
value: '11th 2016 9781319145446 al biology d e ed et freeman h hillis isbn life m of sadava science the w'
30+
31+
journal_nature_medicine:
32+
value: 'medicine nature'
33+
34+
suggested_resource_jstor:
35+
value: 'jstor'
36+
37+
multiple_detections:
38+
value: '103389fpubh202000014 32154200 a air and doi environmental frontiers health impacts in of pmid pollution public review'
39+
40+
citation:
41+
value: '12 2 2005 2007 6 a accessed altun available context current dec education experience httpcieedasueduvolume6number12 hypertext in issues july language learners no of on online reading serial the understanding vol web'

test/fixtures/terms.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,58 @@
22
#
33
# Table name: terms
44
#
5-
# id :integer not null, primary key
6-
# phrase :string
7-
# created_at :datetime not null
8-
# updated_at :datetime not null
9-
# flag :boolean
5+
# id :integer not null, primary key
6+
# phrase :string
7+
# created_at :datetime not null
8+
# updated_at :datetime not null
9+
# flag :boolean
10+
# fingerprint_id :integer
1011
#
1112

1213
cool:
1314
phrase: Super cool search
15+
fingerprint: cool
16+
17+
cool_cluster:
18+
phrase: Super. Cool. Search.
19+
fingerprint: cool
1420

1521
hi:
1622
phrase: hello world
23+
fingerprint: hi
1724

1825
pmid_38908367:
1926
phrase: 'TERT activation targets DNA methylation and multiple aging hallmarks. Shim HS, et al. Cell. 2024. PMID: 38908367'
27+
fingerprint: pmid_38908367
2028

2129
lcsh:
2230
phrase: 'Geology -- Massachusetts'
31+
fingerprint: lcsh
2332

2433
issn_1075_8623:
2534
phrase: 1075-8623
35+
fingerprint: issn_1075_8623
2636

2737
doi:
2838
phrase: '10.1016/j.physio.2010.12.004'
39+
fingerprint: doi
2940

3041
isbn_9781319145446:
3142
phrase: 'Sadava, D. E., D. M. Hillis, et al. Life The Science of Biology. 11th ed. W. H. Freeman, 2016. ISBN: 9781319145446'
43+
fingerprint: isbn_9781319145446
3244

3345
journal_nature_medicine:
3446
phrase: 'nature medicine'
47+
fingerprint: journal_nature_medicine
3548

3649
suggested_resource_jstor:
3750
phrase: 'jstor'
51+
fingerprint: suggested_resource_jstor
3852

3953
multiple_detections:
4054
phrase: 'Environmental and Health Impacts of Air Pollution: A Review. Frontiers in Public Health. PMID: 32154200. DOI: 10.3389/fpubh.2020.00014'
55+
fingerprint: multiple_detections
4156

4257
citation:
4358
phrase: "A. Altun, &quot;Understanding hypertext in the context of reading on the web: Language learners' experience,&quot; Current Issues in Education, vol. 6, no. 12, July, 2005. [Online serial]. Available: http://cie.ed.asu.edu/volume6/number12/. [Accessed Dec. 2, 2007]."
59+
fingerprint: citation

0 commit comments

Comments
 (0)