@natlibfi/melinda-record-matching 5.0.3 → 5.0.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/melinda-node-tests-and-publish.yml +4 -4
- package/dist/candidate-search/query-list/bib.js +84 -67
- package/dist/candidate-search/query-list/bib.js.map +3 -3
- package/dist/candidate-search/query-list/component.js +94 -28
- package/dist/candidate-search/query-list/component.js.map +3 -3
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +2 -2
- package/dist/match-detection/features/bib/f773.js +82 -0
- package/dist/match-detection/features/bib/f773.js.map +7 -0
- package/dist/match-detection/features/bib/index.js +1 -1
- package/dist/match-detection/features/bib/index.js.map +2 -2
- package/dist/match-detection/features/bib/index.test.js +1 -1
- package/dist/match-detection/features/bib/index.test.js.map +2 -2
- package/dist/match-detection/features/bib/isbn.js +47 -48
- package/dist/match-detection/features/bib/isbn.js.map +2 -2
- package/dist/match-detection/features/bib/issn.js +2 -16
- package/dist/match-detection/features/bib/issn.js.map +2 -2
- package/dist/match-detection/features/bib/language.js +12 -22
- package/dist/match-detection/features/bib/language.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +6 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time.js +62 -16
- package/dist/match-detection/features/bib/publication-time.js.map +2 -2
- package/dist/match-detection/features/bib/title.js +76 -26
- package/dist/match-detection/features/bib/title.js.map +3 -3
- package/dist/match-detection/index.js +8 -3
- package/dist/match-detection/index.js.map +2 -2
- package/dist/matching-utils.js +14 -0
- package/dist/matching-utils.js.map +2 -2
- package/package.json +11 -12
- package/src/candidate-search/query-list/bib.js +100 -77
- package/src/candidate-search/query-list/component.js +100 -23
- package/src/cli.js +8 -4
- package/src/match-detection/features/bib/f773.js +118 -0
- package/src/match-detection/features/bib/index.js +1 -1
- package/src/match-detection/features/bib/index.test.js +1 -1
- package/src/match-detection/features/bib/isbn.js +66 -60
- package/src/match-detection/features/bib/issn.js +3 -32
- package/src/match-detection/features/bib/language.js +15 -26
- package/src/match-detection/features/bib/publication-time-allow-cons-years.js +7 -4
- package/src/match-detection/features/bib/publication-time.js +76 -30
- package/src/match-detection/features/bib/title.js +112 -34
- package/src/match-detection/index.js +9 -3
- package/src/matching-utils.js +17 -0
- package/dist/match-detection/features/bib/title-version-original.js +0 -37
- package/dist/match-detection/features/bib/title-version-original.js.map +0 -7
- package/src/match-detection/features/bib/title-version-original.js +0 -52
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
// We should also get copyright time and copyright/publication times from 26x
|
|
2
|
+
// see publication-time-allow-cons-years for a version allowing consequent years to match
|
|
1
3
|
|
|
4
|
+
import {extractPublicationYearFrom773} from '../../../candidate-search/query-list/component.js';
|
|
5
|
+
|
|
6
|
+
const MAX = 0.1;
|
|
7
|
+
const MIN = -1.0;
|
|
2
8
|
|
|
3
|
-
import {testStringOrNumber} from '../../../matching-utils.js';
|
|
4
9
|
|
|
5
|
-
// We should also get copyright time and copyright/publication times from 26x
|
|
6
|
-
// see publication-time-allow-cons-years for a version allowing consequent years to match
|
|
7
10
|
|
|
8
11
|
export default () => ({
|
|
9
12
|
name: 'Publication time',
|
|
10
13
|
extract: ({record}) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
let dateData = getDataFrom008();
|
|
15
|
+
const [f773] = record.get(/^773$/u);
|
|
16
|
+
const hostYear = f773 && extractPublicationYearFrom773(f773) || null;
|
|
17
|
+
dateData.hostYear = hostYear;
|
|
18
|
+
return dateData;
|
|
14
19
|
|
|
15
|
-
function
|
|
20
|
+
function getDataFrom008() {
|
|
16
21
|
const [f008] = record.get(/^008$/u);
|
|
17
22
|
if (!f008 || f008.value.length < 16) {
|
|
18
23
|
return splitDateData('| ');
|
|
@@ -29,50 +34,91 @@ export default () => ({
|
|
|
29
34
|
},
|
|
30
35
|
|
|
31
36
|
compare: (aa, bb) => {
|
|
32
|
-
|
|
37
|
+
// Be happy with a f773$g match:
|
|
38
|
+
if (aa.hostYear && bb.hostYear && aa.hostYear === bb.hostYear) {
|
|
39
|
+
return MAX;
|
|
40
|
+
}
|
|
41
|
+
// Check 008
|
|
42
|
+
if (aa.typeOfDate === 'b') { // 008/06: Before Christ. No really makes sense in our domain, though.
|
|
33
43
|
if (bb.typeOfDate === 'b') {
|
|
34
44
|
return 0;
|
|
35
45
|
}
|
|
36
|
-
return
|
|
46
|
+
return MIN;
|
|
37
47
|
}
|
|
38
48
|
if (aa.typeOfDate === 'n' || bb.typeOfDate === 'n') { // n=unknown
|
|
39
49
|
return 0;
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
// Try to handle questionable dates:
|
|
53
|
+
if (aa.typeOfDate === 'q') { // questionable data
|
|
54
|
+
if (bb.typeOfDate !== 'q') {
|
|
55
|
+
if (yearInBetween(aa.date1, bb.date1, aa.date2)) {
|
|
56
|
+
return MAX;
|
|
57
|
+
}
|
|
58
|
+
return MIN;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// What if there are questionable dates on both sides?
|
|
62
|
+
if (aa.date1 === bb.date1 && bb.date1 === bb.date2) {
|
|
63
|
+
return MAX;
|
|
64
|
+
}
|
|
65
|
+
// Lazily return 0. (We could check for overlap etc. but not really worth the effort)
|
|
66
|
+
return 0.0;
|
|
67
|
+
}
|
|
68
|
+
if (bb.typeOfDate === 'q') {
|
|
69
|
+
if (yearInBetween(bb.date1, aa.date1, bb.date2)) {
|
|
70
|
+
return MAX;
|
|
71
|
+
}
|
|
72
|
+
return MIN;
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
const skipList = [' ', '||||', 'uuuu'];
|
|
43
76
|
if (skipList.includes(aa.date1) || skipList.includes(bb.date1)) { // 008/07-10 carries no information
|
|
44
77
|
return 0;
|
|
45
78
|
}
|
|
46
79
|
|
|
47
80
|
if (matchingYears(aa.date1, bb.date1)) { // 'u' support
|
|
48
|
-
return
|
|
81
|
+
return MAX;
|
|
49
82
|
}
|
|
50
83
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function matchingYears(yyyy1, yyyy2) {
|
|
56
|
-
if (yyyy1.length === 0) { // All digits have been succesfully consumed -> success
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
if (yyyy1[0] === 'u' || yyyy2[0] === 'u') { // Ignore 'u' (it refers to unknown millenia, century, decade or year)
|
|
60
|
-
return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
|
|
61
|
-
}
|
|
62
|
-
if (yyyy1[0] !== yyyy2[0]) {
|
|
63
|
-
return false;
|
|
84
|
+
if (isValidYear(aa.date1) && bb.date1) {
|
|
85
|
+
const difference = Math.abs(aa.date1 - bb.date1);
|
|
86
|
+
if (difference === 1) {
|
|
87
|
+
return MIN/8;
|
|
64
88
|
}
|
|
65
|
-
// Should we require that yyyy[0] is a digit at this point?
|
|
66
|
-
return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
function hasFourDigits(yyyy) { // Will be needed by 'q' support
|
|
71
|
-
return yyyy.match(/^[0-9]{4}$/u);
|
|
72
|
-
}
|
|
73
|
-
*/
|
|
91
|
+
return MIN;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
});
|
|
77
95
|
|
|
96
|
+
function yearInBetween(start, curr, end) {
|
|
97
|
+
if (!isValidYear(start) || !isValidYear(curr) || !isValidYear(end)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return start <= curr && curr <= end;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function matchingYears(yyyy1, yyyy2) {
|
|
104
|
+
if (yyyy1.length === 0) { // All digits have been succesfully consumed -> success
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (yyyy1[0] === 'u' || yyyy2[0] === 'u') { // Ignore 'u' (it refers to unknown millenia, century, decade or year)
|
|
108
|
+
return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
|
|
109
|
+
}
|
|
110
|
+
if (yyyy1[0] !== yyyy2[0]) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (yyyy1[0] >= '0' && yyyy1[0] <= '9') { // Require that yyyy[0] is a digit at this point? (What if year is 983?)
|
|
114
|
+
return matchingYears(yyyy1.slice(1), yyyy2.slice(1));
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
const validYearRegexp = /^(?:1[89][0-9][0-9]|20[012][0-9])$/u;
|
|
78
121
|
|
|
122
|
+
function isValidYear(yyyy) {
|
|
123
|
+
return validYearRegexp.test(yyyy); // Currently supports 1800-2029
|
|
124
|
+
}
|
|
@@ -3,19 +3,28 @@ import naturalPkg from 'natural';
|
|
|
3
3
|
const {LevenshteinDistance: leven} = naturalPkg;
|
|
4
4
|
import {testStringOrNumber} from '../../../matching-utils.js';
|
|
5
5
|
|
|
6
|
-
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:title');
|
|
7
|
-
const debugData = debug.extend('data');
|
|
8
6
|
|
|
7
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title');
|
|
8
|
+
const debugData = debug.extend('data');
|
|
9
9
|
|
|
10
|
-
export default ({threshold =
|
|
10
|
+
export default ({threshold = 0.9} = {}) => ({
|
|
11
11
|
name: 'Title',
|
|
12
12
|
extract: ({record, recordExternal}) => {
|
|
13
13
|
const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
|
|
14
|
-
const
|
|
15
|
-
debug(`${label}: title: ${
|
|
14
|
+
const a = getTitle(record, ['a']);
|
|
15
|
+
debug(`${label}: title: ${a}`);
|
|
16
|
+
|
|
17
|
+
if (a) {
|
|
18
|
+
const b = normalizeTitle(getTitle(record, ['b'])) || '';
|
|
19
|
+
const n = normalizeTitle(getTitle(record, ['n'])) || '';
|
|
20
|
+
const p = normalizeTitle(getTitle(record, ['p'])) || '';
|
|
21
|
+
return [normalizeTitle(a), b, n, p];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return ['', '', '', ''];
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
function normalizeTitle(title) {
|
|
27
|
+
return title
|
|
19
28
|
// decompose unicode diacritics
|
|
20
29
|
.normalize('NFD')
|
|
21
30
|
// strip non-letters/numbers
|
|
@@ -24,48 +33,117 @@ export default ({threshold = 10} = {}) => ({
|
|
|
24
33
|
// - see validator normalize-utf8-diacritics for details
|
|
25
34
|
.replace(/[^\p{Letter}\p{Number}]/gu, '')
|
|
26
35
|
.toLowerCase();
|
|
27
|
-
debug(`${label}: titleString: ${titleAsNormalizedString}`);
|
|
28
|
-
return [titleAsNormalizedString];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
return [];
|
|
32
|
-
|
|
33
|
-
function getTitle() {
|
|
34
|
-
const [field] = record.get(/^245$/u);
|
|
35
|
-
debugData(`${label}: titleField: ${JSON.stringify(field)}`);
|
|
36
|
-
|
|
37
|
-
if (field) {
|
|
38
|
-
return field.subfields
|
|
39
|
-
// get also $n:s and $p:s here
|
|
40
|
-
.filter(({code}) => ['a', 'b', 'n', 'p'].includes(code))
|
|
41
|
-
.map(({value}) => testStringOrNumber(value) ? String(value) : '')
|
|
42
|
-
.join('');
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
38
|
},
|
|
47
39
|
compare: (a, b) => {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
40
|
+
const [aa, ab, an, ap] = a;
|
|
41
|
+
const [ba, bb, bn, bp] = b;
|
|
42
|
+
|
|
43
|
+
if (isEmpty(aa) || isEmpty(ba)) {
|
|
44
|
+
return -1.0;
|
|
45
|
+
}
|
|
46
|
+
// F245$n information is critical; it can not mismatch at all:
|
|
47
|
+
if (an.length && bn.length && an !== bn) { // If these exists, they must be the same (we might convert Roman numbers to Arabic numbers though)
|
|
48
|
+
return -1.0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const aFull = toFullTitle(a);
|
|
52
|
+
const bFull = toFullTitle(b);
|
|
53
|
+
|
|
54
|
+
const [distance, maxLength, correctness] = doLevenshtein(aFull, bFull);
|
|
55
|
+
|
|
56
|
+
debug(`'${aFull}' vs '${bFull}': Max length = ${maxLength}, distance = ${distance}, correctness = ${correctness}`);
|
|
50
57
|
|
|
51
58
|
if (distance === 0) {
|
|
52
59
|
return 0.5;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
if (correctness >= threshold) {
|
|
63
|
+
return 0.4;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (an && bn) {
|
|
67
|
+
// There seems to be some wobble between $b and $p, for example:
|
|
68
|
+
if (ab && isEmpty(ap) && bp && isEmpty(bb)) {
|
|
69
|
+
return compare([aa, ap, an, ab], [ba, bb, bn, bp]);
|
|
70
|
+
}
|
|
71
|
+
if (ap && isEmpty(ab) && bb && isEmpty(bp)) {
|
|
72
|
+
return compare([aa, ab, an, ap], [ba, bp, bn, bb]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try the same without $p:
|
|
77
|
+
if (localXor(ap, bp)) {
|
|
78
|
+
const result = compare([aa, ab, an, ''], [ba, bb, bn, '']);
|
|
79
|
+
return result > 0.0 ? result * 0.8 : result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isEmpty(ap) && isEmpty(bp) && localXor(ab, bb)) {
|
|
83
|
+
// Try the same without $b ($p is not here)
|
|
84
|
+
const result = compare([aa, '', an, ''], [ba, '', bn, '']);
|
|
85
|
+
return result > 0.0 ? result * 0.8 : result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return -0.5; // Not likely
|
|
89
|
+
|
|
90
|
+
function isEmpty(x) {
|
|
91
|
+
return !x || x.length === 0;
|
|
92
|
+
}
|
|
57
93
|
|
|
58
|
-
|
|
94
|
+
function localXor(x, y) {
|
|
95
|
+
if (isEmpty(x)) {
|
|
96
|
+
return !isEmpty(y);
|
|
97
|
+
}
|
|
98
|
+
// 'x' exists, thus 'y' can not exist:
|
|
99
|
+
return isEmpty(y);
|
|
100
|
+
}
|
|
59
101
|
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
function doLevenshtein(string1, string2) {
|
|
103
|
+
const distance = leven(string1, string2);
|
|
104
|
+
const len = getMaxLength(string1, string2);
|
|
105
|
+
const correctness = 1.0 - (distance / len);
|
|
106
|
+
return [distance, len, correctness];
|
|
62
107
|
}
|
|
63
108
|
|
|
64
|
-
|
|
109
|
+
function toFullTitle(arr) {
|
|
110
|
+
const relevant = arr.filter(val => typeof val === 'string' && val.length);
|
|
111
|
+
return relevant.join(' ');
|
|
112
|
+
}
|
|
65
113
|
|
|
66
|
-
function getMaxLength() {
|
|
67
|
-
return
|
|
114
|
+
function getMaxLength(str1, str2) {
|
|
115
|
+
return str1.length > str2.length ? str1.length : str2.length;
|
|
68
116
|
}
|
|
69
117
|
|
|
70
118
|
}
|
|
71
119
|
});
|
|
120
|
+
|
|
121
|
+
export function getTitle(record, targetSubfieldCodes = ['a', 'b', 'n', 'p']) {
|
|
122
|
+
const [field] = record.get(/^245$/u);
|
|
123
|
+
debugData(`titleField: ${JSON.stringify(field)}`);
|
|
124
|
+
|
|
125
|
+
if (field) {
|
|
126
|
+
const title = field.subfields
|
|
127
|
+
// get also $n:s and $p:s here
|
|
128
|
+
.filter(({code}) => targetSubfieldCodes.includes(code))
|
|
129
|
+
// Would be nice to normalize $n values...
|
|
130
|
+
.map(({value}) => testStringOrNumber(value) ? String(value) : '')
|
|
131
|
+
.join(' ')
|
|
132
|
+
.replace(/\[[^\]]*\]/ug, ' ') // Remove [whatever] stuff. ADD TEST FOR THIS
|
|
133
|
+
.replace(/ [=\/:](?:$| )/ug, ' ') // Strip punctuation
|
|
134
|
+
// Also đ vs d pairs seen:
|
|
135
|
+
//.replace(/(?:đ|ȧ|t̄|ǩ|ǧ|s̆|c̆)/ug, '') // Hack. Saamelaisbibliografia has often dropped this wierd characters (oft old articles, no longer used in sami either)
|
|
136
|
+
// trim:
|
|
137
|
+
.replace(/ +/ug, ' ')
|
|
138
|
+
.replace(/^ +/u, '')
|
|
139
|
+
.replace(/ +$/u, '');
|
|
140
|
+
|
|
141
|
+
// Skip non-filing indicator (note that '9' is a magic indicator value, so we don't do it):
|
|
142
|
+
if (/^[1-8]$/u.test(field.ind2)) { // Skip non-filing characters
|
|
143
|
+
return title.slice(parseInt(field.ind2));
|
|
144
|
+
}
|
|
145
|
+
return title;
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
@@ -5,7 +5,8 @@ import * as features from './features/index.js';
|
|
|
5
5
|
export {features};
|
|
6
6
|
|
|
7
7
|
export default ({strategy, threshold = 0.8999}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {
|
|
8
|
-
const
|
|
8
|
+
const MIN_PROPABILITY_QUALIFIER = 0.4; // Title perfect match: 0.5
|
|
9
|
+
const FORCE_FAIL = -1.0;
|
|
9
10
|
|
|
10
11
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');
|
|
11
12
|
const debugData = debug.extend('data');
|
|
@@ -41,13 +42,18 @@ export default ({strategy, threshold = 0.8999}, returnStrategy = false, localSet
|
|
|
41
42
|
const featurePairs = generateFeaturePairs(featuresA, featuresB);
|
|
42
43
|
const similarityVector = generateSimilarityVector(featurePairs);
|
|
43
44
|
|
|
44
|
-
if (similarityVector.some(v => v >=
|
|
45
|
+
if (similarityVector.some(v => v >= MIN_PROPABILITY_QUALIFIER)) {
|
|
45
46
|
const probability = calculateProbability(similarityVector);
|
|
46
47
|
debug(`probability: ${probability} (Threshold: ${threshold})`);
|
|
48
|
+
if (similarityVector.some(v => v <= FORCE_FAIL)) {
|
|
49
|
+
debug(`Some feature resulted in utter failure (${FORCE_FAIL} or less)`);
|
|
50
|
+
return returnResult({match: false, probability: 0.0});
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
return returnResult({match: probability >= threshold, probability});
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
debugData(`No feature yielded minimum probability amount of points (${
|
|
56
|
+
debugData(`No feature yielded minimum probability amount of points (${MIN_PROPABILITY_QUALIFIER})`);
|
|
51
57
|
return returnResult({match: false, probability: 0.0});
|
|
52
58
|
}
|
|
53
59
|
|
package/src/matching-utils.js
CHANGED
|
@@ -112,3 +112,20 @@ export function getMatchCounts(aValues, bValues) {
|
|
|
112
112
|
return aToB < bToA ? aToB : bToA;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
export function stringAfter(string, substring) {
|
|
118
|
+
const pos = string.indexOf(substring);
|
|
119
|
+
if (pos === -1) {
|
|
120
|
+
return string;
|
|
121
|
+
}
|
|
122
|
+
return string.substring(pos+substring.length);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function stringBefore(string, substring) {
|
|
126
|
+
const pos = string.indexOf(substring);
|
|
127
|
+
if (pos === -1) {
|
|
128
|
+
return string;
|
|
129
|
+
}
|
|
130
|
+
return string.substring(0, pos);
|
|
131
|
+
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import createDebugLogger from "debug";
|
|
2
|
-
import naturalPkg from "natural";
|
|
3
|
-
const { LevenshteinDistance: leven } = naturalPkg;
|
|
4
|
-
export default ({ threshold = 10 } = {}) => ({
|
|
5
|
-
name: "titleVersionOriginal",
|
|
6
|
-
extract: ({ record }) => {
|
|
7
|
-
const title = getTitle();
|
|
8
|
-
if (title) {
|
|
9
|
-
return [title.replace(/[^\p{Letter}\p{Number}]/gu, "").toLowerCase()];
|
|
10
|
-
}
|
|
11
|
-
return [];
|
|
12
|
-
function getTitle() {
|
|
13
|
-
const [field] = record.get(/^245$/u);
|
|
14
|
-
if (field) {
|
|
15
|
-
return field.subfields.filter(({ code }) => ["a", "b"].includes(code)).map(({ value }) => value).join("");
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
compare: (a, b) => {
|
|
20
|
-
const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features/bib/title-version-original");
|
|
21
|
-
const distance = leven(a[0], b[0]);
|
|
22
|
-
if (distance === 0) {
|
|
23
|
-
return 0.5;
|
|
24
|
-
}
|
|
25
|
-
const maxLength = getMaxLength();
|
|
26
|
-
const percentage = distance / maxLength * 100;
|
|
27
|
-
debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
|
|
28
|
-
if (percentage <= threshold) {
|
|
29
|
-
return 0.3;
|
|
30
|
-
}
|
|
31
|
-
return -0.5;
|
|
32
|
-
function getMaxLength() {
|
|
33
|
-
return a[0].length > b[0].length ? a[0].length : b[0].length;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
//# sourceMappingURL=title-version-original.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../../src/match-detection/features/bib/title-version-original.js"],
|
|
4
|
-
"sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport naturalPkg from 'natural';\nconst {LevenshteinDistance: leven} = naturalPkg;\n\nexport default ({threshold = 10} = {}) => ({\n name: 'titleVersionOriginal',\n extract: ({record}) => {\n const title = getTitle();\n\n if (title) {\n return [title.replace(/[^\\p{Letter}\\p{Number}]/gu, '').toLowerCase()];\n }\n\n return [];\n\n function getTitle() {\n const [field] = record.get(/^245$/u);\n\n if (field) {\n return field.subfields\n .filter(({code}) => ['a', 'b'].includes(code))\n .map(({value}) => value)\n .join('');\n }\n }\n },\n compare: (a, b) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title-version-original');\n const distance = leven(a[0], b[0]);\n\n if (distance === 0) {\n return 0.5;\n }\n\n const maxLength = getMaxLength();\n const percentage = distance / maxLength * 100;\n\n debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);\n\n if (percentage <= threshold) {\n return 0.3;\n }\n\n return -0.5;\n\n function getMaxLength() {\n return a[0].length > b[0].length ? a[0].length : b[0].length;\n }\n\n }\n});\n"],
|
|
5
|
-
"mappings": "AACA,OAAO,uBAAuB;AAC9B,OAAO,gBAAgB;AACvB,MAAM,EAAC,qBAAqB,MAAK,IAAI;AAErC,eAAe,CAAC,EAAC,YAAY,GAAE,IAAI,CAAC,OAAO;AAAA,EACzC,MAAM;AAAA,EACN,SAAS,CAAC,EAAC,OAAM,MAAM;AACrB,UAAM,QAAQ,SAAS;AAEvB,QAAI,OAAO;AACT,aAAO,CAAC,MAAM,QAAQ,6BAA6B,EAAE,EAAE,YAAY,CAAC;AAAA,IACtE;AAEA,WAAO,CAAC;AAER,aAAS,WAAW;AAClB,YAAM,CAAC,KAAK,IAAI,OAAO,IAAI,QAAQ;AAEnC,UAAI,OAAO;AACT,eAAO,MAAM,UACV,OAAO,CAAC,EAAC,KAAI,MAAM,CAAC,KAAK,GAAG,EAAE,SAAS,IAAI,CAAC,EAC5C,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK,EACtB,KAAK,EAAE;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,CAAC,GAAG,MAAM;AACjB,UAAM,QAAQ,kBAAkB,uFAAuF;AACvH,UAAM,WAAW,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;AAEjC,QAAI,aAAa,GAAG;AAClB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,aAAa;AAC/B,UAAM,aAAa,WAAW,YAAY;AAE1C,UAAM,IAAI,CAAC,SAAS,CAAC,mBAAmB,SAAS,gBAAgB,QAAQ,kBAAkB,UAAU,EAAE;AAEvG,QAAI,cAAc,WAAW;AAC3B,aAAO;AAAA,IACT;AAEA,WAAO;AAEP,aAAS,eAAe;AACtB,aAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,IACxD;AAAA,EAEF;AACF;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import createDebugLogger from 'debug';
|
|
3
|
-
import naturalPkg from 'natural';
|
|
4
|
-
const {LevenshteinDistance: leven} = naturalPkg;
|
|
5
|
-
|
|
6
|
-
export default ({threshold = 10} = {}) => ({
|
|
7
|
-
name: 'titleVersionOriginal',
|
|
8
|
-
extract: ({record}) => {
|
|
9
|
-
const title = getTitle();
|
|
10
|
-
|
|
11
|
-
if (title) {
|
|
12
|
-
return [title.replace(/[^\p{Letter}\p{Number}]/gu, '').toLowerCase()];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return [];
|
|
16
|
-
|
|
17
|
-
function getTitle() {
|
|
18
|
-
const [field] = record.get(/^245$/u);
|
|
19
|
-
|
|
20
|
-
if (field) {
|
|
21
|
-
return field.subfields
|
|
22
|
-
.filter(({code}) => ['a', 'b'].includes(code))
|
|
23
|
-
.map(({value}) => value)
|
|
24
|
-
.join('');
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
compare: (a, b) => {
|
|
29
|
-
const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/title-version-original');
|
|
30
|
-
const distance = leven(a[0], b[0]);
|
|
31
|
-
|
|
32
|
-
if (distance === 0) {
|
|
33
|
-
return 0.5;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const maxLength = getMaxLength();
|
|
37
|
-
const percentage = distance / maxLength * 100;
|
|
38
|
-
|
|
39
|
-
debug(`'${a}' vs '${b}': Max length = ${maxLength}, distance = ${distance}, percentage = ${percentage}`);
|
|
40
|
-
|
|
41
|
-
if (percentage <= threshold) {
|
|
42
|
-
return 0.3;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return -0.5;
|
|
46
|
-
|
|
47
|
-
function getMaxLength() {
|
|
48
|
-
return a[0].length > b[0].length ? a[0].length : b[0].length;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
});
|