@natlibfi/melinda-record-matching 5.1.6 → 5.1.7-alpha.1
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/dist/candidate-search/candidate-search-utils.js +5 -3
- package/dist/candidate-search/candidate-search-utils.js.map +2 -2
- package/dist/candidate-search/query-list/bib.js +2 -2
- package/dist/candidate-search/query-list/bib.js.map +2 -2
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +2 -2
- package/dist/index.js +19 -16
- package/dist/index.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +2 -2
- package/dist/matching-utils.js +2 -2
- package/dist/matching-utils.js.map +2 -2
- package/package.json +1 -1
- package/src/candidate-search/candidate-search-utils.js +9 -4
- package/src/candidate-search/query-list/bib.js +2 -2
- package/src/cli.js +18 -1
- package/src/index.js +24 -18
- package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +1 -0
- package/src/matching-utils.js +2 -1
|
@@ -2,10 +2,12 @@ import createDebugLogger from "debug";
|
|
|
2
2
|
export function toQueries(identifiers, queryString) {
|
|
3
3
|
const debug = createDebugLogger("@natlibfi/melinda-record-matching:toQueries");
|
|
4
4
|
const debugData = debug.extend("data");
|
|
5
|
-
const quotedIdentifiers = identifiers.map((identifier) => identifier.match(/\//u) ? `"${identifier}"` : `${identifier}`);
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const quotedIdentifiers = identifiers.map((identifier) => identifier.match(/\//u) || identifier.match(/\^/u) ? `"${identifier}"` : `${identifier}`);
|
|
6
|
+
const caretPairs = toPairs(quotedIdentifiers.filter((identifier) => identifier.match(/\^/u)));
|
|
7
|
+
const nonCaretPairs = toPairs(quotedIdentifiers.filter((identifier) => !identifier.match(/\^/u)));
|
|
8
|
+
const pairs = nonCaretPairs.concat(caretPairs);
|
|
8
9
|
debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);
|
|
10
|
+
const queries = pairs.map(([a, b]) => b ? `${queryString}=${a} or ${queryString}=${b}` : `${queryString}=${a}`);
|
|
9
11
|
debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);
|
|
10
12
|
return queries;
|
|
11
13
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/candidate-search/candidate-search-utils.js"],
|
|
4
|
-
"sourcesContent": ["import createDebugLogger from 'debug';\n\nexport function toQueries(identifiers, queryString) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:toQueries');\n const debugData = debug.extend('data');\n\n // We quote the identifier, if it contains a slash! (Slash in non-quoted SRU-search breaks search.)\n const quotedIdentifiers = identifiers.map(identifier => identifier.match(/\\//u) ? `\"${identifier}\"` : `${identifier}`);\n\n // Aleph supports only two queries with or -operator (This is not actually true)\n const
|
|
5
|
-
"mappings": "AAAA,OAAO,uBAAuB;AAEvB,gBAAS,UAAU,aAAa,aAAa;AAElD,QAAM,QAAQ,kBAAkB,6CAA6C;AAC7E,QAAM,YAAY,MAAM,OAAO,MAAM;
|
|
4
|
+
"sourcesContent": ["import createDebugLogger from 'debug';\n\nexport function toQueries(identifiers, queryString) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:toQueries');\n const debugData = debug.extend('data');\n\n // We quote the identifier, if it contains a slash! (Slash in non-quoted SRU-search breaks search.)\n // We also quote the identifier, if it starts with caret (f028 searches fail without caret and quotes...)\n const quotedIdentifiers = identifiers.map(identifier => identifier.match(/\\//u) || identifier.match(/\\^/u) ? `\"${identifier}\"` : `${identifier}`);\n\n // We can't pair queries with starting caret and without (ie. left anchored queries with non-left anchored queries)\n const caretPairs = toPairs(quotedIdentifiers.filter(identifier => identifier.match(/\\^/u)));\n const nonCaretPairs = toPairs(quotedIdentifiers.filter(identifier => !identifier.match(/\\^/u)));\n\n const pairs = nonCaretPairs.concat(caretPairs);\n debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);\n\n // Aleph supports only two queries with or -operator (This is not actually true)\n const queries = pairs.map(([a, b]) => b ? `${queryString}=${a} or ${queryString}=${b}` : `${queryString}=${a}`);\n debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);\n\n return queries;\n}\n\nfunction toPairs(array) {\n if (array.length === 0) {\n return [];\n }\n return [array.slice(0, 2)].concat(toPairs(array.slice(2), 2));\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,uBAAuB;AAEvB,gBAAS,UAAU,aAAa,aAAa;AAElD,QAAM,QAAQ,kBAAkB,6CAA6C;AAC7E,QAAM,YAAY,MAAM,OAAO,MAAM;AAIrC,QAAM,oBAAoB,YAAY,IAAI,gBAAc,WAAW,MAAM,KAAK,KAAK,WAAW,MAAM,KAAK,IAAI,IAAI,UAAU,MAAM,GAAG,UAAU,EAAE;AAGhJ,QAAM,aAAa,QAAQ,kBAAkB,OAAO,gBAAc,WAAW,MAAM,KAAK,CAAC,CAAC;AAC1F,QAAM,gBAAgB,QAAQ,kBAAkB,OAAO,gBAAc,CAAC,WAAW,MAAM,KAAK,CAAC,CAAC;AAE9F,QAAM,QAAQ,cAAc,OAAO,UAAU;AAC7C,YAAU,UAAU,MAAM,MAAM,MAAM,KAAK,UAAU,KAAK,CAAC,EAAE;AAG7D,QAAM,UAAU,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,WAAW,IAAI,CAAC,OAAO,WAAW,IAAI,CAAC,KAAK,GAAG,WAAW,IAAI,CAAC,EAAE;AAC9G,YAAU,YAAY,QAAQ,MAAM,MAAM,KAAK,UAAU,OAAO,CAAC,EAAE;AAEnE,SAAO;AACT;AAEA,SAAS,QAAQ,OAAO;AACtB,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,CAAC;AAAA,EACV;AACA,SAAO,CAAC,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,OAAO,QAAQ,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;AAC9D;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -287,9 +287,9 @@ export function bibStandardIdentifiers(record) {
|
|
|
287
287
|
if (a) {
|
|
288
288
|
const b = subfields.find((sf) => sf.code === "b");
|
|
289
289
|
if (b) {
|
|
290
|
-
return [a.value
|
|
290
|
+
return [`^${a.value}`, `^${b.value} ${a.value}`];
|
|
291
291
|
}
|
|
292
|
-
return [a.value];
|
|
292
|
+
return [`^${a.value}`];
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
return subfields.filter((sub) => ["a", "z"].includes(sub.code) && testStringOrNumber(sub.value) && otherIdReqExp.test(String(sub.value))).map(({ value }) => String(value));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/candidate-search/query-list/bib.js"],
|
|
4
|
-
"sourcesContent": ["import createDebugLogger from 'debug';\nimport {toQueries} from '../candidate-search-utils.js';\nimport {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues, testStringOrNumber} from '../../matching-utils.js';\nimport {extractHostMelindaIdsFromField, extractPublicationYearFrom773, getHostMelindaFields} from './component.js';\nimport {fieldToString} from '@natlibfi/marc-record-validators-melinda';\nimport {getTitle} from '../../match-detection/features/bib/title.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query');\n\nconst IS_OK_ALONE_THRESHOLD = 5;\n\nexport function bibSourceIds(record) {\n\n /* Melinda's SRU-index melinda.sourceid includes source IDs from SID fields in Melinda records\n SID-fields in Melinda have sf $c with local id and sf $b with a code for the local db:\n\n SID__ $c 123457 $b helka\n SID__ $c (ANDL100020)1077305 $b sata\n SID__ $c VER999999 $ FI-KV\n SID__ $c /10024/508126 $ REPO_THESEUS\n\n In melinda.sourceid -index case is kept, sourceprefixes in brackets and hyphens are normalized away:\n Note: slashes are not normalized away, but a SRU-search-string including slashes needs to be quoted\n\n 1234567helka\n 1077305sata\n VER999999FIKV\n /10024/508126REPO_THESEUS\n\n Note: All Melinda records that have a matching records in a local db do NOT have SID for that local records,\n existence of a SID field depends on how the record has been added to Melinda and how it has been handled\n afterwards. SIDs are also not reliably maintained. A record might or might not have a SID for a local db\n after the matching record is removed from the local db.\n\n */\n\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:source-ids');\n const debugData = debug.extend('data');\n //const debugInfo = debug.extend('info');\n\n debug(`Creating queries for sourceid's`);\n\n const fSids = record.get('SID');\n debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);\n\n return fSids.length > 0 ? toSidQueries(fSids) : [];\n\n function toSidQueries(fSids) {\n debug(`Creating actual queries for sourceid's`);\n\n const sidStrings = getSidStrings(fSids);\n\n if (sidStrings.length < 1) {\n debug(`No identifiers found, no queries created.`);\n return [];\n }\n\n const sidQueries = toQueries(sidStrings, 'melinda.sourceid');\n\n return sidQueries;\n\n function getSidStrings(fSids) {\n debug(`Getting Sid strings from SID fields`);\n\n // Map SID fields to valid sidStrings, filter out empty strings\n const sidStrings = fSids.map(toSidString).filter(nonEmptySid => nonEmptySid);\n return sidStrings;\n\n function toSidString(field) {\n debug(`Getting string from a field`);\n\n return validateSidFieldSubfieldCounts(field) ? createSidString(field) : '';\n\n function createSidString(field) {\n debug(`Creating string from a field`);\n const [sfC] = getSubfieldValues(field, 'c');\n const [sfB] = getSubfieldValues(field, 'b');\n\n const cleanedSfC = removeSourcePrefix(normalizeSidSubfieldValue(sfC));\n const cleanedSfB = normalizeSidSubfieldValue(sfB);\n\n debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);\n return cleanedSfC.concat(cleanedSfB);\n }\n\n function removeSourcePrefix(subfieldValue) {\n const sourcePrefixRegex = (/^(?<sourcePrefix>\\([A-Za-z0-9-]+\\))(?<id>.+)$/u);\n const normalizedValue = testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(sourcePrefixRegex, '$<id>') : '';\n debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);\n return normalizedValue;\n }\n\n function normalizeSidSubfieldValue(subfieldValue) {\n debugData(`Normalizing ${subfieldValue}`);\n const normalizeAwayRegex = (/[- ]/u);\n return testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(normalizeAwayRegex, '') : '';\n }\n\n }\n }\n }\n}\n\nexport function bibMelindaIds(record) {\n // Melinda's SRU-index melinda.melindaid includes f001 controlnumbers and old Melinda-IDs from f035z's for all non-deleted Melinda-records\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibMelindaIds');\n const debugData = debug.extend('data');\n debug(`Creating queries for MelindaIds`);\n\n // Note: Melinda-ID's for search queries are created just from records f035a's and f035z's\n // Both (FI-MELINDA)- and FCC-prefixed forms are found\n // f001 controlnumber is not currently included, even if record's f003 is FI-MELINDA\n const melindaIds = getMelindaIdsF035(record);\n\n debugData(`Unique identifiers (${melindaIds.length}): ${JSON.stringify(melindaIds)}`);\n\n if (melindaIds.length < 1) {\n debug(`No identifiers found, no queries created.`);\n return [];\n }\n\n return toQueries(melindaIds, 'melinda.melindaid');\n}\n\n\n// bibHostComponents returns host id from the first subfield $w of first field f773, see test-fixtures 04 and 05\n// bibHostComponents should search all 773 $ws for possible host id, but what should it do in case of multiple host ids?\nexport function bibHostComponents(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents');\n const debugData = debug.extend('data');\n debug(`Creating queries for hostIds`);\n const f773s = getHostMelindaFields(record);\n if (f773s.length !== 1) { // This needs some thinking...\n return [];\n }\n\n const [id] = extractHostMelindaIdsFromField(f773s[0]);\n debug(`Found id: ${JSON.stringify(id)}`);\n\n // NB! record.isCR() does not work if LDR/07=a, so we get year from 773$g!\n const year = extractPublicationYearFrom773(f773s[0]);\n debug(`${fieldToString(f773s[0])} => ${year ? year : 'N/A'}`);\n if (year) {\n return [`dc.date=${year} AND melinda.partsofhost=${id}`];\n }\n\n return [`melinda.partsofhost=${id}`];\n\n}\n\n\n\n\n// SRU search dc.title with a search phrase starting with ^ maps currently in Melinda to (probably) to *headings* index TIT\n// - Aleph cannot currently handle headings searches starting with a boolean - in these cases use word search\n\nexport function bibTitle(record) {\n // We get author/publisher only when formatted title is shorter than 5 chars\n return bibTitleAuthorPublisher({record, onlyTitleLength: 5});\n}\n\nexport function bibTitleAuthor(record) {\n debug('bibTitleAuthor');\n // We use onlyTitleLength that is longer than our formatted length to\n // get an author or an publisher always\n return bibTitleAuthorPublisher({record, onlyTitleLength: 100});\n}\n\nexport function bibTitleAuthorYear(record) {\n debug('bibTitleAuthorYearAlternates');\n // We use onlyTitleLength that is longer than our formatted length to\n // get an author or an publisher always\n\n return bibTitleAuthorPublisher({record, onlyTitleLength: 100, addYear: true});\n}\n\nexport function bibTitleAuthorYearAlternates(record) {\n debug('bibTitleAuthorYearAlternates');\n // We use onlyTitleLength that is longer than our formatted length to\n // get an author or an publisher always\n const origQueryList = bibTitleAuthorPublisher({record, onlyTitleLength: 100, addYear: true, alternates: true, alternateQueries: []});\n debug(`${JSON.stringify(origQueryList)}`);\n return {queryList: Array.from(origQueryList).reverse(), queryListType: 'alternates'};\n}\n\nfunction getTitleForQuery(record) {\n const title = getTitle(record, ['a']);\n if (title) {\n const nWords = title.split(' ');\n // If lone $a is deemed too short, fetch $b as well:\n // (NV: I've seen pairs with 245$a-only vs 245$a$b, so I'd like to use $a only if it is long enough)\n\n if (nWords < 3 || title.length < 12) {\n const [f245] = record.get('245');\n // If punctuation is ' =' I think that f245$a is good enough despite shortness. Trying to balance between two bad situations...\n // \"Suo (= short title) siell\u00E4, vetel\u00E4 (= missing $b name in another language) t\u00E4\u00E4ll\u00E4...\"\n if (f245 && f245.subfields.find(sf => sf.code === 'a' && sf.value.match(/ =$/u)) && title.length >= IS_OK_ALONE_THRESHOLD) {\n return title;\n }\n return getTitle(record, ['a', 'b']); // Try to get a longer title\n }\n }\n return title;\n}\n\nfunction dcTitle(record, onlyTitleLength, alternates = false) {\n const title = getTitleForQuery(record);\n if (!testStringOrNumber(title)) {\n return [];\n }\n\n const formatted = getFormattedTitle(title);\n\n // use word search for titles starting with a boolean\n const useWordSearch = checkUseWordSearch(formatted);\n // Prevent too many matches / SRU crashing by having a minimum length\n // Note that currently this fails matching if there are no matches from previous matchers\n if (formatted.length >= onlyTitleLength && !alternates) {\n return [`dc.title=\"${useWordSearch ? '' : '^'}${formatted}*\"`, formatted, true];\n }\n\n const queryIsOkAlone = !useWordSearch && formatted.length >= IS_OK_ALONE_THRESHOLD;\n\n // use word search without ending * also in combination searches to avoid SRU-server crashes [MRA-189]\n return [`dc.title=\"${useWordSearch || !queryIsOkAlone ? '' : '^'}${formatted}${queryIsOkAlone ? '*' : ''}\"`, formatted, queryIsOkAlone];\n\n function getFormattedTitle(title) {\n const formatted = String(title)\n .replace(/[\\-+ !\"(){}\\[\\]<>;:.?/@*%=^_`~]/gu, ' ')\n .replace(/[^\\w\\s\\p{Alphabetic}]/gu, '') // Apparently matches to/works with non-aplhabetic scripts such as Chinese as well\n .replace(/ +/gu, ' ') // Clean up concurrent spaces from eg. subfield changes\n .trim()\n .replace(/^(.{30}\\S*) .*$/, \"$1\")\n .trim();\n\n return formatted;\n }\n}\n\nexport function bibTitleAuthorPublisher({record, onlyTitleLength, addYear = false, alternates = false, alternateQueries = []}) {\n debug(`bibTitleAuthorPublisher, onlyTitleLength: ${onlyTitleLength}, addYear: ${addYear}, alternates: ${alternates}`);\n const [query, formatted, queryIsOkAlone] = dcTitle(record, onlyTitleLength, alternates);\n if (query === undefined) {\n return [];\n }\n\n debug(`query: ${query}`);\n\n // Prevent too many matches / SRU crashing by having a minimum length\n // Note that currently this fails matching if there are no matches from previous matchers\n if (formatted.length >= onlyTitleLength && !alternates) {\n return [query];\n }\n\n const newAlternateQueries = alternates ? [...alternateQueries, query] : alternateQueries;\n\n return addAuthorsToSearch({query, queryIsOkAlone, addYear, alternates, alternateQueries: newAlternateQueries});\n\n function addAuthorsToSearch({query, queryIsOkAlone = false, addYear = false, alternates = false, alternateQueries = []}) {\n debug('addAuthorsToSearch');\n const [authorQuery] = bibAuthors(record);\n if (authorQuery !== undefined) {\n if (addYear) {\n const newAlternateQueries = alternates ? [...alternateQueries, `${authorQuery} AND ${query}`] : alternateQueries;\n return addYearToSearch({query: `${authorQuery} AND ${query}`, queryIsOkAlone: true, alternates, alternateQueries: newAlternateQueries});\n }\n return alternates ? alternateQueries : [`${authorQuery} AND ${query}`];\n }\n return addPublisherToSearch({query, queryIsOkAlone, addYear, alternates, alternateQueries});\n //return [];\n }\n\n function addPublisherToSearch({query, queryIsOkAlone = false, addYear = false, alternates = false, alternateQueries = []}) {\n const [publisherQuery] = bibPublishers(record);\n if (publisherQuery !== undefined) {\n if (addYear) {\n const newAlternateQueries = alternates ? [...alternateQueries, `${publisherQuery} AND ${query}`] : alternateQueries;\n return addYearToSearch({query: `${publisherQuery} AND ${query}`, queryIsOkAlone: true, alternates, alternateQueries: newAlternateQueries});\n }\n return alternates ? alternateQueries : [`${publisherQuery} AND ${query}`];\n }\n if (queryIsOkAlone && !addYear) {\n return alternates ? alternateQueries : [`${query}`];\n }\n return addYearToSearch({query, queryIsOkAlone, alternates, alternateQueries});\n }\n\n function addYearToSearch({query, queryIsOkAlone = false, alternates = false, alternateQueries = []}) {\n const [yearQuery] = bibYear(record);\n if (yearQuery !== undefined) {\n const yearAndOtherQueries = `${yearQuery} AND ${query}`;\n return alternates ? [...alternateQueries, yearAndOtherQueries] : [yearAndOtherQueries];\n }\n if (queryIsOkAlone) {\n return alternates ? alternateQueries : [`${query}`];\n }\n return [];\n }\n\n}\n\nexport function bibAuthors(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibAuthors');\n const debugData = debug.extend('data');\n debug(`Creating query for the first author`);\n //debugData(record);\n\n const author = getAuthor(record);\n\n if (testStringOrNumber(author)) {\n const formatted = String(author)\n .replace(/[^\\w\\s\\p{Alphabetic}]/gu, '')\n // Clean up concurrent spaces from fe. subfield changes\n .replace(/ +/gu, ' ')\n .trim()\n .slice(0, 30)\n .trim();\n\n // use word search for authors starting with a boolean\n const useWordSearch = checkUseWordSearch(formatted);\n // Prevent too many matches by having a minimum length\n debugData(`Author string: ${formatted}`);\n if (formatted.length >= 5) {\n return [`dc.author=\"${useWordSearch ? '' : '^'}${formatted}*\"`];\n }\n return [];\n }\n\n return [];\n\n function getAuthor(record) {\n //debugData(record);\n const [field] = record.get(/^(?:100|110|111|700|710|711)$/u);\n //debugData(field);\n\n if (field) {\n const authorString = field.subfields\n .filter(({code}) => ['a'].includes(code)) // We might use different subfield code sets for X00, X10 and X11?\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value)\n // In Melinda's index subfield separators are indexed as ' '\n .join(' ');\n return authorString;\n }\n return false;\n }\n}\n\nexport function bibPublishers(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibPublishers');\n const debugData = debug.extend('data');\n debug(`Creating query for the first publisher`);\n //debugData(record);\n\n const publisher = getPublisher(record);\n if (testStringOrNumber(publisher)) {\n const formatted = String(publisher)\n .replace(/[^\\w\\s\\p{Alphabetic}]/gu, '')\n // Clean up concurrent spaces from fe. subfield changes\n .replace(/ +/gu, ' ')\n .trim()\n .slice(0, 30)\n .trim();\n\n debugData(`Publisher string: '${formatted}'`);\n if (formatted.length === 0) {\n return [];\n }\n // use non-wildcard word search from dc.publisher\n return [`dc.publisher=\"${formatted}\"`];\n }\n\n return [];\n\n function getPublisher(record) {\n //debugData(record);\n const [field] = record.get(/^26[04]$/u).filter(f => f.subfields.some(sf => sf.code === 'b')); // Filter removes copyright-only fields\n //debugData(field);\n\n if (field) {\n const publisherString = field.subfields\n .filter(({code}) => ['b'].includes(code))\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value)\n // In Melinda's index subfield separators are indexed as ' '\n .join(' ')\n .trim();\n return publisherString;\n }\n return false;\n }\n}\n\nexport function bibYear(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibYear');\n const debugData = debug.extend('data');\n debug(`Creating query for the publishing year`);\n\n const year = getYear(record);\n if (year) {\n return [`dc.date=\"${year}\"`];\n }\n return [];\n\n function getYear(record) {\n const [f008] = record.get(/^008$/u);\n if (!f008 || !f008.value || f008.value.length < 11) {\n debug('f008 missing/crappy');\n return false;\n }\n\n debugData(`f008: ${JSON.stringify(f008)}`);\n const {value} = f008;\n if (value === '||||' || value === ' ') { // Meaningless values\n return false;\n }\n return value.slice(7, 11);\n }\n}\n\nexport function checkUseWordSearch(formatted) {\n const lowercased = formatted.toLowerCase();\n // Note: add a space to startWords to catch just actual boolean words\n const booleanStartWords = ['and ', 'or ', 'nor ', 'not '];\n return booleanStartWords.some(word => lowercased.startsWith(word));\n}\n\nexport function bibStandardIdentifiers(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibStandardIdentifiers');\n const debugData = debug.extend('data');\n debug(`Creating queries for standard identifiers`);\n\n // DEVELOP: should we query also f015 and f028?\n\n // const fields = record.get(/^(?<def>020|022|024)$/u);\n const fields = record.get(/^(?<def>015|020|022|024|028)$/u);\n const identifiers = [].concat(...fields.map(toIdentifiers));\n const uniqueIdentifiers = [...new Set(identifiers)];\n\n debugData(`Standard identifier fields: ${JSON.stringify(fields)}`);\n debugData(`Identifiers (${identifiers.length}): ${JSON.stringify(identifiers)}`);\n debugData(`Unique identifiers (${uniqueIdentifiers.length}): ${JSON.stringify(uniqueIdentifiers)}`);\n\n if (uniqueIdentifiers.length < 1) {\n debug(`No identifiers found, no queries created.`);\n return [];\n }\n\n return toQueries(uniqueIdentifiers, 'dc.identifier');\n\n function toIdentifiers({tag, subfields}) {\n const issnIsbnReqExp = (/^[A-Za-z0-9-]+$/u);\n const otherIdReqExp = (/^[A-Za-z0-9-:]+$/u);\n\n if (tag === '015') { // TODO: test (does this use the right index etc.)\n return subfields\n .filter(sf => sf.code === 'a' && sf.value.match(/^[A-Z0-9\\-]+$/ui))\n .map(sf => sf.value)\n }\n\n if (tag === '020') {\n return subfields\n .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))\n .map(({value}) => String(value));\n }\n\n if (tag === '022') {\n return subfields\n .filter(sub => ['a', 'z', 'y'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))\n .map(({value}) => String(value));\n }\n\n if (tag === '028') { // TODO: test\n const a = subfields.find(sf => sf.code === 'a');\n if (a) {\n const b = subfields.find(sf => sf.code === 'b');\n // TODO: normalize?\n if (b) {\n return [a.value, `${b.value} ${a.value}`];\n }\n return [a.value];\n }\n }\n\n // Default seems to be 024:\n return subfields\n .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && otherIdReqExp.test(String(sub.value)))\n .map(({value}) => String(value));\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,uBAAuB;AAC9B,SAAQ,iBAAgB;AACxB,SAAQ,mBAAmB,gCAAgC,mBAAmB,0BAAyB;AACvG,SAAQ,gCAAgC,+BAA+B,4BAA2B;AAClG,SAAQ,qBAAoB;AAC5B,SAAQ,gBAAe;AAEvB,MAAM,QAAQ,kBAAkB,0DAA0D;AAE1F,MAAM,wBAAwB;AAEvB,gBAAS,aAAa,QAAQ;AA0BnC,QAAMA,SAAQ,kBAAkB,qEAAqE;AACrG,QAAM,YAAYA,OAAM,OAAO,MAAM;AAGrC,EAAAA,OAAM,iCAAiC;AAEvC,QAAM,QAAQ,OAAO,IAAI,KAAK;AAC9B,YAAU,eAAe,MAAM,MAAM,MAAM,KAAK,UAAU,KAAK,CAAC,EAAE;AAElE,SAAO,MAAM,SAAS,IAAI,aAAa,KAAK,IAAI,CAAC;AAEjD,WAAS,aAAaC,QAAO;AAC3B,IAAAD,OAAM,wCAAwC;AAE9C,UAAM,aAAa,cAAcC,MAAK;AAEtC,QAAI,WAAW,SAAS,GAAG;AACzB,MAAAD,OAAM,2CAA2C;AACjD,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,aAAa,UAAU,YAAY,kBAAkB;AAE3D,WAAO;AAEP,aAAS,cAAcC,QAAO;AAC5B,MAAAD,OAAM,qCAAqC;AAG3C,YAAME,cAAaD,OAAM,IAAI,WAAW,EAAE,OAAO,iBAAe,WAAW;AAC3E,aAAOC;AAEP,eAAS,YAAY,OAAO;AAC1B,QAAAF,OAAM,6BAA6B;AAEnC,eAAO,+BAA+B,KAAK,IAAI,gBAAgB,KAAK,IAAI;AAExE,iBAAS,gBAAgBG,QAAO;AAC9B,UAAAH,OAAM,8BAA8B;AACpC,gBAAM,CAAC,GAAG,IAAI,kBAAkBG,QAAO,GAAG;AAC1C,gBAAM,CAAC,GAAG,IAAI,kBAAkBA,QAAO,GAAG;AAE1C,gBAAM,aAAa,mBAAmB,0BAA0B,GAAG,CAAC;AACpE,gBAAM,aAAa,0BAA0B,GAAG;AAEhD,oBAAU,GAAG,KAAK,UAAU,GAAG,CAAC,MAAM,KAAK,UAAU,GAAG,CAAC,EAAE;AAC3D,iBAAO,WAAW,OAAO,UAAU;AAAA,QACrC;AAEA,iBAAS,mBAAmB,eAAe;AACzC,gBAAM,oBAAqB;AAC3B,gBAAM,kBAAkB,mBAAmB,aAAa,IAAI,OAAO,aAAa,EAAE,QAAQ,mBAAmB,OAAO,IAAI;AACxH,oBAAU,cAAc,aAAa,OAAO,eAAe,EAAE;AAC7D,iBAAO;AAAA,QACT;AAEA,iBAAS,0BAA0B,eAAe;AAChD,oBAAU,eAAe,aAAa,EAAE;AACxC,gBAAM,qBAAsB;AAC5B,iBAAO,mBAAmB,aAAa,IAAI,OAAO,aAAa,EAAE,QAAQ,oBAAoB,EAAE,IAAI;AAAA,QACrG;AAAA,MAEF;AAAA,IACF;AAAA,EACF;AACF;AAEO,gBAAS,cAAc,QAAQ;AAGpC,QAAMH,SAAQ,kBAAkB,wEAAwE;AACxG,QAAM,YAAYA,OAAM,OAAO,MAAM;AACrC,EAAAA,OAAM,iCAAiC;AAKvC,QAAM,aAAa,kBAAkB,MAAM;AAE3C,YAAU,uBAAuB,WAAW,MAAM,MAAM,KAAK,UAAU,UAAU,CAAC,EAAE;AAEpF,MAAI,WAAW,SAAS,GAAG;AACzB,IAAAA,OAAM,2CAA2C;AACjD,WAAO,CAAC;AAAA,EACV;AAEA,SAAO,UAAU,YAAY,mBAAmB;AAClD;AAKO,gBAAS,kBAAkB,QAAQ;AACxC,QAAMA,SAAQ,kBAAkB,4EAA4E;AAC5G,QAAM,YAAYA,OAAM,OAAO,MAAM;AACrC,EAAAA,OAAM,8BAA8B;AACpC,QAAM,QAAQ,qBAAqB,MAAM;AACzC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,CAAC,EAAE,IAAI,+BAA+B,MAAM,CAAC,CAAC;AACpD,EAAAA,OAAM,aAAa,KAAK,UAAU,EAAE,CAAC,EAAE;AAGvC,QAAM,OAAO,8BAA8B,MAAM,CAAC,CAAC;AACnD,EAAAA,OAAM,GAAG,cAAc,MAAM,CAAC,CAAC,CAAC,OAAO,OAAO,OAAO,KAAK,EAAE;AAC5D,MAAI,MAAM;AACR,WAAO,CAAC,WAAW,IAAI,4BAA4B,EAAE,EAAE;AAAA,EACzD;AAEA,SAAO,CAAC,uBAAuB,EAAE,EAAE;AAErC;AAQO,gBAAS,SAAS,QAAQ;AAE/B,SAAO,wBAAwB,EAAC,QAAQ,iBAAiB,EAAC,CAAC;AAC7D;AAEO,gBAAS,eAAe,QAAQ;AACrC,QAAM,gBAAgB;AAGtB,SAAO,wBAAwB,EAAC,QAAQ,iBAAiB,IAAG,CAAC;AAC/D;AAEO,gBAAS,mBAAmB,QAAQ;AACzC,QAAM,8BAA8B;AAIpC,SAAO,wBAAwB,EAAC,QAAQ,iBAAiB,KAAK,SAAS,KAAI,CAAC;AAC9E;AAEO,gBAAS,6BAA6B,QAAQ;AACnD,QAAM,8BAA8B;AAGpC,QAAM,gBAAgB,wBAAwB,EAAC,QAAQ,iBAAiB,KAAK,SAAS,MAAM,YAAY,MAAM,kBAAkB,CAAC,EAAC,CAAC;AACnI,QAAM,GAAG,KAAK,UAAU,aAAa,CAAC,EAAE;AACxC,SAAO,EAAC,WAAW,MAAM,KAAK,aAAa,EAAE,QAAQ,GAAG,eAAe,aAAY;AACrF;AAEA,SAAS,iBAAiB,QAAQ;AAChC,QAAM,QAAQ,SAAS,QAAQ,CAAC,GAAG,CAAC;AACpC,MAAI,OAAO;AACT,UAAM,SAAS,MAAM,MAAM,GAAG;AAI9B,QAAI,SAAS,KAAK,MAAM,SAAS,IAAI;AACnC,YAAM,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK;AAG/B,UAAI,QAAQ,KAAK,UAAU,KAAK,QAAM,GAAG,SAAS,OAAO,GAAG,MAAM,MAAM,MAAM,CAAC,KAAK,MAAM,UAAU,uBAAuB;AACzH,eAAO;AAAA,MACT;AACA,aAAO,SAAS,QAAQ,CAAC,KAAK,GAAG,CAAC;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,QAAQ,iBAAiB,aAAa,OAAO;AAC5D,QAAM,QAAQ,iBAAiB,MAAM;AACrC,MAAI,CAAC,mBAAmB,KAAK,GAAG;AAC9B,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,YAAY,kBAAkB,KAAK;AAGzC,QAAM,gBAAgB,mBAAmB,SAAS;AAGlD,MAAI,UAAU,UAAU,mBAAmB,CAAC,YAAY;AACtD,WAAO,CAAC,aAAa,gBAAgB,KAAK,GAAG,GAAG,SAAS,MAAM,WAAW,IAAI;AAAA,EAChF;AAEA,QAAM,iBAAiB,CAAC,iBAAiB,UAAU,UAAU;AAG7D,SAAO,CAAC,aAAa,iBAAiB,CAAC,iBAAiB,KAAK,GAAG,GAAG,SAAS,GAAG,iBAAiB,MAAM,EAAE,KAAK,WAAW,cAAc;AAEtI,WAAS,kBAAkBI,QAAO;AAChC,UAAMC,aAAY,OAAOD,MAAK,EAC3B,QAAQ,qCAAqC,GAAG,EAChD,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,SAAS,GAAG,EACpB,KAAK,EACL,QAAQ,mBAAmB,IAAI,EAC/B,KAAK;AAER,WAAOC;AAAA,EACT;AACF;AAEO,gBAAS,wBAAwB,EAAC,QAAQ,iBAAiB,UAAU,OAAO,aAAa,OAAO,mBAAmB,CAAC,EAAC,GAAG;AAC7H,QAAM,6CAA6C,eAAe,cAAc,OAAO,iBAAiB,UAAU,EAAE;AACpH,QAAM,CAAC,OAAO,WAAW,cAAc,IAAI,QAAQ,QAAQ,iBAAiB,UAAU;AACtF,MAAI,UAAU,QAAW;AACvB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,UAAU,KAAK,EAAE;AAIvB,MAAI,UAAU,UAAU,mBAAmB,CAAC,YAAY;AACtD,WAAO,CAAC,KAAK;AAAA,EACf;AAEA,QAAM,sBAAsB,aAAa,CAAC,GAAG,kBAAkB,KAAK,IAAI;AAExE,SAAO,mBAAmB,EAAC,OAAO,gBAAgB,SAAS,YAAY,kBAAkB,oBAAmB,CAAC;AAE7G,WAAS,mBAAmB,EAAC,OAAAC,QAAO,gBAAAC,kBAAiB,OAAO,SAAAC,WAAU,OAAO,YAAAC,cAAa,OAAO,kBAAAC,oBAAmB,CAAC,EAAC,GAAG;AACvH,UAAM,oBAAoB;AAC1B,UAAM,CAAC,WAAW,IAAI,WAAW,MAAM;AACvC,QAAI,gBAAgB,QAAW;AAC7B,UAAIF,UAAS;AACX,cAAMG,uBAAsBF,cAAa,CAAC,GAAGC,mBAAkB,GAAG,WAAW,QAAQJ,MAAK,EAAE,IAAII;AAChG,eAAO,gBAAgB,EAAC,OAAO,GAAG,WAAW,QAAQJ,MAAK,IAAI,gBAAgB,MAAM,YAAAG,aAAY,kBAAkBE,qBAAmB,CAAC;AAAA,MACxI;AACA,aAAOF,cAAaC,oBAAmB,CAAC,GAAG,WAAW,QAAQJ,MAAK,EAAE;AAAA,IACvE;AACA,WAAO,qBAAqB,EAAC,OAAAA,QAAO,gBAAAC,iBAAgB,SAAAC,UAAS,YAAAC,aAAY,kBAAAC,kBAAgB,CAAC;AAAA,EAE5F;AAEA,WAAS,qBAAqB,EAAC,OAAAJ,QAAO,gBAAAC,kBAAiB,OAAO,SAAAC,WAAU,OAAO,YAAAC,cAAa,OAAO,kBAAAC,oBAAmB,CAAC,EAAC,GAAG;AACzH,UAAM,CAAC,cAAc,IAAI,cAAc,MAAM;AAC7C,QAAI,mBAAmB,QAAW;AAChC,UAAIF,UAAS;AACX,cAAMG,uBAAsBF,cAAa,CAAC,GAAGC,mBAAkB,GAAG,cAAc,QAAQJ,MAAK,EAAE,IAAII;AACnG,eAAO,gBAAgB,EAAC,OAAO,GAAG,cAAc,QAAQJ,MAAK,IAAI,gBAAgB,MAAM,YAAAG,aAAY,kBAAkBE,qBAAmB,CAAC;AAAA,MAC3I;AACA,aAAOF,cAAaC,oBAAmB,CAAC,GAAG,cAAc,QAAQJ,MAAK,EAAE;AAAA,IAC1E;AACA,QAAIC,mBAAkB,CAACC,UAAS;AAC9B,aAAOC,cAAaC,oBAAmB,CAAC,GAAGJ,MAAK,EAAE;AAAA,IACpD;AACA,WAAO,gBAAgB,EAAC,OAAAA,QAAO,gBAAAC,iBAAgB,YAAAE,aAAY,kBAAAC,kBAAgB,CAAC;AAAA,EAC9E;AAEA,WAAS,gBAAgB,EAAC,OAAAJ,QAAO,gBAAAC,kBAAiB,OAAO,YAAAE,cAAa,OAAO,kBAAAC,oBAAmB,CAAC,EAAC,GAAG;AACnG,UAAM,CAAC,SAAS,IAAI,QAAQ,MAAM;AAClC,QAAI,cAAc,QAAW;AAC3B,YAAM,sBAAsB,GAAG,SAAS,QAAQJ,MAAK;AACrD,aAAOG,cAAa,CAAC,GAAGC,mBAAkB,mBAAmB,IAAI,CAAC,mBAAmB;AAAA,IACvF;AACA,QAAIH,iBAAgB;AAClB,aAAOE,cAAaC,oBAAmB,CAAC,GAAGJ,MAAK,EAAE;AAAA,IACpD;AACA,WAAO,CAAC;AAAA,EACV;AAEF;AAEO,gBAAS,WAAW,QAAQ;AACjC,QAAMN,SAAQ,kBAAkB,qEAAqE;AACrG,QAAM,YAAYA,OAAM,OAAO,MAAM;AACrC,EAAAA,OAAM,qCAAqC;AAG3C,QAAM,SAAS,UAAU,MAAM;AAE/B,MAAI,mBAAmB,MAAM,GAAG;AAC9B,UAAM,YAAY,OAAO,MAAM,EAC5B,QAAQ,2BAA2B,EAAE,EAErC,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,EAAE,EACX,KAAK;AAGR,UAAM,gBAAgB,mBAAmB,SAAS;AAElD,cAAU,kBAAkB,SAAS,EAAE;AACvC,QAAI,UAAU,UAAU,GAAG;AACzB,aAAO,CAAC,cAAc,gBAAgB,KAAK,GAAG,GAAG,SAAS,IAAI;AAAA,IAChE;AACA,WAAO,CAAC;AAAA,EACV;AAEA,SAAO,CAAC;AAER,WAAS,UAAUY,SAAQ;AAEzB,UAAM,CAAC,KAAK,IAAIA,QAAO,IAAI,gCAAgC;AAG3D,QAAI,OAAO;AACT,YAAM,eAAe,MAAM,UACxB,OAAO,CAAC,EAAC,KAAI,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,EACvC,IAAI,CAAC,EAAC,MAAK,MAAM,mBAAmB,KAAK,IAAI,OAAO,KAAK,IAAI,EAAE,EAC/D,OAAO,WAAS,KAAK,EAErB,KAAK,GAAG;AACX,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACF;AAEO,gBAAS,cAAc,QAAQ;AACpC,QAAMZ,SAAQ,kBAAkB,wEAAwE;AACxG,QAAM,YAAYA,OAAM,OAAO,MAAM;AACrC,EAAAA,OAAM,wCAAwC;AAG9C,QAAM,YAAY,aAAa,MAAM;AACrC,MAAI,mBAAmB,SAAS,GAAG;AACjC,UAAM,YAAY,OAAO,SAAS,EAC/B,QAAQ,2BAA2B,EAAE,EAErC,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,EAAE,EACX,KAAK;AAER,cAAU,sBAAsB,SAAS,GAAG;AAC5C,QAAI,UAAU,WAAW,GAAG;AAC1B,aAAO,CAAC;AAAA,IACV;AAEA,WAAO,CAAC,iBAAiB,SAAS,GAAG;AAAA,EACvC;AAEA,SAAO,CAAC;AAER,WAAS,aAAaY,SAAQ;AAE5B,UAAM,CAAC,KAAK,IAAIA,QAAO,IAAI,WAAW,EAAE,OAAO,OAAK,EAAE,UAAU,KAAK,QAAM,GAAG,SAAS,GAAG,CAAC;AAG3F,QAAI,OAAO;AACT,YAAM,kBAAkB,MAAM,UAC3B,OAAO,CAAC,EAAC,KAAI,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,CAAC,EACvC,IAAI,CAAC,EAAC,MAAK,MAAM,mBAAmB,KAAK,IAAI,OAAO,KAAK,IAAI,EAAE,EAC/D,OAAO,WAAS,KAAK,EAErB,KAAK,GAAG,EACR,KAAK;AACR,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACF;AAEO,gBAAS,QAAQ,QAAQ;AAC9B,QAAMZ,SAAQ,kBAAkB,kEAAkE;AAClG,QAAM,YAAYA,OAAM,OAAO,MAAM;AACrC,EAAAA,OAAM,wCAAwC;AAE9C,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,MAAM;AACR,WAAO,CAAC,YAAY,IAAI,GAAG;AAAA,EAC7B;AACA,SAAO,CAAC;AAER,WAAS,QAAQY,SAAQ;AACvB,UAAM,CAAC,IAAI,IAAIA,QAAO,IAAI,QAAQ;AAClC,QAAI,CAAC,QAAQ,CAAC,KAAK,SAAS,KAAK,MAAM,SAAS,IAAI;AAClD,MAAAZ,OAAM,qBAAqB;AAC3B,aAAO;AAAA,IACT;AAEA,cAAU,SAAS,KAAK,UAAU,IAAI,CAAC,EAAE;AACzC,UAAM,EAAC,MAAK,IAAI;AAChB,QAAI,UAAU,UAAU,UAAU,QAAQ;AACxC,aAAO;AAAA,IACT;AACA,WAAO,MAAM,MAAM,GAAG,EAAE;AAAA,EAC1B;AACF;AAEO,gBAAS,mBAAmB,WAAW;AAC5C,QAAM,aAAa,UAAU,YAAY;AAEzC,QAAM,oBAAoB,CAAC,QAAQ,OAAO,QAAQ,MAAM;AACxD,SAAO,kBAAkB,KAAK,UAAQ,WAAW,WAAW,IAAI,CAAC;AACnE;AAEO,gBAAS,uBAAuB,QAAQ;AAE7C,QAAMA,SAAQ,kBAAkB,iFAAiF;AACjH,QAAM,YAAYA,OAAM,OAAO,MAAM;AACrC,EAAAA,OAAM,2CAA2C;AAKjD,QAAM,SAAS,OAAO,IAAI,gCAAgC;AAC1D,QAAM,cAAc,CAAC,EAAE,OAAO,GAAG,OAAO,IAAI,aAAa,CAAC;AAC1D,QAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,WAAW,CAAC;AAElD,YAAU,+BAA+B,KAAK,UAAU,MAAM,CAAC,EAAE;AACjE,YAAU,gBAAgB,YAAY,MAAM,MAAM,KAAK,UAAU,WAAW,CAAC,EAAE;AAC/E,YAAU,uBAAuB,kBAAkB,MAAM,MAAM,KAAK,UAAU,iBAAiB,CAAC,EAAE;AAElG,MAAI,kBAAkB,SAAS,GAAG;AAChC,IAAAA,OAAM,2CAA2C;AACjD,WAAO,CAAC;AAAA,EACV;AAEA,SAAO,UAAU,mBAAmB,eAAe;AAEnD,WAAS,cAAc,EAAC,KAAK,UAAS,GAAG;AACvC,UAAM,iBAAkB;AACxB,UAAM,gBAAiB;AAEvB,QAAI,QAAQ,OAAO;AACjB,aAAO,UACJ,OAAO,QAAM,GAAG,SAAS,OAAO,GAAG,MAAM,MAAM,iBAAiB,CAAC,EACjE,IAAI,QAAM,GAAG,KAAK;AAAA,IACvB;AAEA,QAAI,QAAQ,OAAO;AACjB,aAAO,UACJ,OAAO,SAAO,CAAC,KAAK,GAAG,EAAE,SAAS,IAAI,IAAI,KAAK,mBAAmB,IAAI,KAAK,KAAK,eAAe,KAAK,OAAO,IAAI,KAAK,CAAC,CAAC,EACtH,IAAI,CAAC,EAAC,MAAK,MAAM,OAAO,KAAK,CAAC;AAAA,IACnC;AAEA,QAAI,QAAQ,OAAO;AACjB,aAAO,UACJ,OAAO,SAAO,CAAC,KAAK,KAAK,GAAG,EAAE,SAAS,IAAI,IAAI,KAAK,mBAAmB,IAAI,KAAK,KAAK,eAAe,KAAK,OAAO,IAAI,KAAK,CAAC,CAAC,EAC3H,IAAI,CAAC,EAAC,MAAK,MAAM,OAAO,KAAK,CAAC;AAAA,IACnC;AAEA,QAAI,QAAQ,OAAO;AACjB,YAAM,IAAI,UAAU,KAAK,QAAM,GAAG,SAAS,GAAG;AAC9C,UAAI,GAAG;AACL,cAAM,IAAI,UAAU,KAAK,QAAM,GAAG,SAAS,GAAG;AAE9C,YAAI,GAAG;AACL,iBAAO,CAAC,EAAE,
|
|
4
|
+
"sourcesContent": ["import createDebugLogger from 'debug';\nimport {toQueries} from '../candidate-search-utils.js';\nimport {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues, testStringOrNumber} from '../../matching-utils.js';\nimport {extractHostMelindaIdsFromField, extractPublicationYearFrom773, getHostMelindaFields} from './component.js';\nimport {fieldToString} from '@natlibfi/marc-record-validators-melinda';\nimport {getTitle} from '../../match-detection/features/bib/title.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query');\n\nconst IS_OK_ALONE_THRESHOLD = 5;\n\nexport function bibSourceIds(record) {\n\n /* Melinda's SRU-index melinda.sourceid includes source IDs from SID fields in Melinda records\n SID-fields in Melinda have sf $c with local id and sf $b with a code for the local db:\n\n SID__ $c 123457 $b helka\n SID__ $c (ANDL100020)1077305 $b sata\n SID__ $c VER999999 $ FI-KV\n SID__ $c /10024/508126 $ REPO_THESEUS\n\n In melinda.sourceid -index case is kept, sourceprefixes in brackets and hyphens are normalized away:\n Note: slashes are not normalized away, but a SRU-search-string including slashes needs to be quoted\n\n 1234567helka\n 1077305sata\n VER999999FIKV\n /10024/508126REPO_THESEUS\n\n Note: All Melinda records that have a matching records in a local db do NOT have SID for that local records,\n existence of a SID field depends on how the record has been added to Melinda and how it has been handled\n afterwards. SIDs are also not reliably maintained. A record might or might not have a SID for a local db\n after the matching record is removed from the local db.\n\n */\n\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:source-ids');\n const debugData = debug.extend('data');\n //const debugInfo = debug.extend('info');\n\n debug(`Creating queries for sourceid's`);\n\n const fSids = record.get('SID');\n debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);\n\n return fSids.length > 0 ? toSidQueries(fSids) : [];\n\n function toSidQueries(fSids) {\n debug(`Creating actual queries for sourceid's`);\n\n const sidStrings = getSidStrings(fSids);\n\n if (sidStrings.length < 1) {\n debug(`No identifiers found, no queries created.`);\n return [];\n }\n\n const sidQueries = toQueries(sidStrings, 'melinda.sourceid');\n\n return sidQueries;\n\n function getSidStrings(fSids) {\n debug(`Getting Sid strings from SID fields`);\n\n // Map SID fields to valid sidStrings, filter out empty strings\n const sidStrings = fSids.map(toSidString).filter(nonEmptySid => nonEmptySid);\n return sidStrings;\n\n function toSidString(field) {\n debug(`Getting string from a field`);\n\n return validateSidFieldSubfieldCounts(field) ? createSidString(field) : '';\n\n function createSidString(field) {\n debug(`Creating string from a field`);\n const [sfC] = getSubfieldValues(field, 'c');\n const [sfB] = getSubfieldValues(field, 'b');\n\n const cleanedSfC = removeSourcePrefix(normalizeSidSubfieldValue(sfC));\n const cleanedSfB = normalizeSidSubfieldValue(sfB);\n\n debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);\n return cleanedSfC.concat(cleanedSfB);\n }\n\n function removeSourcePrefix(subfieldValue) {\n const sourcePrefixRegex = (/^(?<sourcePrefix>\\([A-Za-z0-9-]+\\))(?<id>.+)$/u);\n const normalizedValue = testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(sourcePrefixRegex, '$<id>') : '';\n debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);\n return normalizedValue;\n }\n\n function normalizeSidSubfieldValue(subfieldValue) {\n debugData(`Normalizing ${subfieldValue}`);\n const normalizeAwayRegex = (/[- ]/u);\n return testStringOrNumber(subfieldValue) ? String(subfieldValue).replace(normalizeAwayRegex, '') : '';\n }\n\n }\n }\n }\n}\n\nexport function bibMelindaIds(record) {\n // Melinda's SRU-index melinda.melindaid includes f001 controlnumbers and old Melinda-IDs from f035z's for all non-deleted Melinda-records\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibMelindaIds');\n const debugData = debug.extend('data');\n debug(`Creating queries for MelindaIds`);\n\n // Note: Melinda-ID's for search queries are created just from records f035a's and f035z's\n // Both (FI-MELINDA)- and FCC-prefixed forms are found\n // f001 controlnumber is not currently included, even if record's f003 is FI-MELINDA\n const melindaIds = getMelindaIdsF035(record);\n\n debugData(`Unique identifiers (${melindaIds.length}): ${JSON.stringify(melindaIds)}`);\n\n if (melindaIds.length < 1) {\n debug(`No identifiers found, no queries created.`);\n return [];\n }\n\n return toQueries(melindaIds, 'melinda.melindaid');\n}\n\n\n// bibHostComponents returns host id from the first subfield $w of first field f773, see test-fixtures 04 and 05\n// bibHostComponents should search all 773 $ws for possible host id, but what should it do in case of multiple host ids?\nexport function bibHostComponents(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents');\n const debugData = debug.extend('data');\n debug(`Creating queries for hostIds`);\n const f773s = getHostMelindaFields(record);\n if (f773s.length !== 1) { // This needs some thinking...\n return [];\n }\n\n const [id] = extractHostMelindaIdsFromField(f773s[0]);\n debug(`Found id: ${JSON.stringify(id)}`);\n\n // NB! record.isCR() does not work if LDR/07=a, so we get year from 773$g!\n const year = extractPublicationYearFrom773(f773s[0]);\n debug(`${fieldToString(f773s[0])} => ${year ? year : 'N/A'}`);\n if (year) {\n return [`dc.date=${year} AND melinda.partsofhost=${id}`];\n }\n\n return [`melinda.partsofhost=${id}`];\n\n}\n\n\n\n\n// SRU search dc.title with a search phrase starting with ^ maps currently in Melinda to (probably) to *headings* index TIT\n// - Aleph cannot currently handle headings searches starting with a boolean - in these cases use word search\n\nexport function bibTitle(record) {\n // We get author/publisher only when formatted title is shorter than 5 chars\n return bibTitleAuthorPublisher({record, onlyTitleLength: 5});\n}\n\nexport function bibTitleAuthor(record) {\n debug('bibTitleAuthor');\n // We use onlyTitleLength that is longer than our formatted length to\n // get an author or an publisher always\n return bibTitleAuthorPublisher({record, onlyTitleLength: 100});\n}\n\nexport function bibTitleAuthorYear(record) {\n debug('bibTitleAuthorYearAlternates');\n // We use onlyTitleLength that is longer than our formatted length to\n // get an author or an publisher always\n\n return bibTitleAuthorPublisher({record, onlyTitleLength: 100, addYear: true});\n}\n\nexport function bibTitleAuthorYearAlternates(record) {\n debug('bibTitleAuthorYearAlternates');\n // We use onlyTitleLength that is longer than our formatted length to\n // get an author or an publisher always\n const origQueryList = bibTitleAuthorPublisher({record, onlyTitleLength: 100, addYear: true, alternates: true, alternateQueries: []});\n debug(`${JSON.stringify(origQueryList)}`);\n return {queryList: Array.from(origQueryList).reverse(), queryListType: 'alternates'};\n}\n\nfunction getTitleForQuery(record) {\n const title = getTitle(record, ['a']);\n if (title) {\n const nWords = title.split(' ');\n // If lone $a is deemed too short, fetch $b as well:\n // (NV: I've seen pairs with 245$a-only vs 245$a$b, so I'd like to use $a only if it is long enough)\n\n if (nWords < 3 || title.length < 12) {\n const [f245] = record.get('245');\n // If punctuation is ' =' I think that f245$a is good enough despite shortness. Trying to balance between two bad situations...\n // \"Suo (= short title) siell\u00E4, vetel\u00E4 (= missing $b name in another language) t\u00E4\u00E4ll\u00E4...\"\n if (f245 && f245.subfields.find(sf => sf.code === 'a' && sf.value.match(/ =$/u)) && title.length >= IS_OK_ALONE_THRESHOLD) {\n return title;\n }\n return getTitle(record, ['a', 'b']); // Try to get a longer title\n }\n }\n return title;\n}\n\nfunction dcTitle(record, onlyTitleLength, alternates = false) {\n const title = getTitleForQuery(record);\n if (!testStringOrNumber(title)) {\n return [];\n }\n\n const formatted = getFormattedTitle(title);\n\n // use word search for titles starting with a boolean\n const useWordSearch = checkUseWordSearch(formatted);\n // Prevent too many matches / SRU crashing by having a minimum length\n // Note that currently this fails matching if there are no matches from previous matchers\n if (formatted.length >= onlyTitleLength && !alternates) {\n return [`dc.title=\"${useWordSearch ? '' : '^'}${formatted}*\"`, formatted, true];\n }\n\n const queryIsOkAlone = !useWordSearch && formatted.length >= IS_OK_ALONE_THRESHOLD;\n\n // use word search without ending * also in combination searches to avoid SRU-server crashes [MRA-189]\n return [`dc.title=\"${useWordSearch || !queryIsOkAlone ? '' : '^'}${formatted}${queryIsOkAlone ? '*' : ''}\"`, formatted, queryIsOkAlone];\n\n function getFormattedTitle(title) {\n const formatted = String(title)\n .replace(/[\\-+ !\"(){}\\[\\]<>;:.?/@*%=^_`~]/gu, ' ')\n .replace(/[^\\w\\s\\p{Alphabetic}]/gu, '') // Apparently matches to/works with non-aplhabetic scripts such as Chinese as well\n .replace(/ +/gu, ' ') // Clean up concurrent spaces from eg. subfield changes\n .trim()\n .replace(/^(.{30}\\S*) .*$/, \"$1\")\n .trim();\n\n return formatted;\n }\n}\n\nexport function bibTitleAuthorPublisher({record, onlyTitleLength, addYear = false, alternates = false, alternateQueries = []}) {\n debug(`bibTitleAuthorPublisher, onlyTitleLength: ${onlyTitleLength}, addYear: ${addYear}, alternates: ${alternates}`);\n const [query, formatted, queryIsOkAlone] = dcTitle(record, onlyTitleLength, alternates);\n if (query === undefined) {\n return [];\n }\n\n debug(`query: ${query}`);\n\n // Prevent too many matches / SRU crashing by having a minimum length\n // Note that currently this fails matching if there are no matches from previous matchers\n if (formatted.length >= onlyTitleLength && !alternates) {\n return [query];\n }\n\n const newAlternateQueries = alternates ? [...alternateQueries, query] : alternateQueries;\n\n return addAuthorsToSearch({query, queryIsOkAlone, addYear, alternates, alternateQueries: newAlternateQueries});\n\n function addAuthorsToSearch({query, queryIsOkAlone = false, addYear = false, alternates = false, alternateQueries = []}) {\n debug('addAuthorsToSearch');\n const [authorQuery] = bibAuthors(record);\n if (authorQuery !== undefined) {\n if (addYear) {\n const newAlternateQueries = alternates ? [...alternateQueries, `${authorQuery} AND ${query}`] : alternateQueries;\n return addYearToSearch({query: `${authorQuery} AND ${query}`, queryIsOkAlone: true, alternates, alternateQueries: newAlternateQueries});\n }\n return alternates ? alternateQueries : [`${authorQuery} AND ${query}`];\n }\n return addPublisherToSearch({query, queryIsOkAlone, addYear, alternates, alternateQueries});\n //return [];\n }\n\n function addPublisherToSearch({query, queryIsOkAlone = false, addYear = false, alternates = false, alternateQueries = []}) {\n const [publisherQuery] = bibPublishers(record);\n if (publisherQuery !== undefined) {\n if (addYear) {\n const newAlternateQueries = alternates ? [...alternateQueries, `${publisherQuery} AND ${query}`] : alternateQueries;\n return addYearToSearch({query: `${publisherQuery} AND ${query}`, queryIsOkAlone: true, alternates, alternateQueries: newAlternateQueries});\n }\n return alternates ? alternateQueries : [`${publisherQuery} AND ${query}`];\n }\n if (queryIsOkAlone && !addYear) {\n return alternates ? alternateQueries : [`${query}`];\n }\n return addYearToSearch({query, queryIsOkAlone, alternates, alternateQueries});\n }\n\n function addYearToSearch({query, queryIsOkAlone = false, alternates = false, alternateQueries = []}) {\n const [yearQuery] = bibYear(record);\n if (yearQuery !== undefined) {\n const yearAndOtherQueries = `${yearQuery} AND ${query}`;\n return alternates ? [...alternateQueries, yearAndOtherQueries] : [yearAndOtherQueries];\n }\n if (queryIsOkAlone) {\n return alternates ? alternateQueries : [`${query}`];\n }\n return [];\n }\n\n}\n\nexport function bibAuthors(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibAuthors');\n const debugData = debug.extend('data');\n debug(`Creating query for the first author`);\n //debugData(record);\n\n const author = getAuthor(record);\n\n if (testStringOrNumber(author)) {\n const formatted = String(author)\n .replace(/[^\\w\\s\\p{Alphabetic}]/gu, '')\n // Clean up concurrent spaces from fe. subfield changes\n .replace(/ +/gu, ' ')\n .trim()\n .slice(0, 30)\n .trim();\n\n // use word search for authors starting with a boolean\n const useWordSearch = checkUseWordSearch(formatted);\n // Prevent too many matches by having a minimum length\n debugData(`Author string: ${formatted}`);\n if (formatted.length >= 5) {\n return [`dc.author=\"${useWordSearch ? '' : '^'}${formatted}*\"`];\n }\n return [];\n }\n\n return [];\n\n function getAuthor(record) {\n //debugData(record);\n const [field] = record.get(/^(?:100|110|111|700|710|711)$/u);\n //debugData(field);\n\n if (field) {\n const authorString = field.subfields\n .filter(({code}) => ['a'].includes(code)) // We might use different subfield code sets for X00, X10 and X11?\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value)\n // In Melinda's index subfield separators are indexed as ' '\n .join(' ');\n return authorString;\n }\n return false;\n }\n}\n\nexport function bibPublishers(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibPublishers');\n const debugData = debug.extend('data');\n debug(`Creating query for the first publisher`);\n //debugData(record);\n\n const publisher = getPublisher(record);\n if (testStringOrNumber(publisher)) {\n const formatted = String(publisher)\n .replace(/[^\\w\\s\\p{Alphabetic}]/gu, '')\n // Clean up concurrent spaces from fe. subfield changes\n .replace(/ +/gu, ' ')\n .trim()\n .slice(0, 30)\n .trim();\n\n debugData(`Publisher string: '${formatted}'`);\n if (formatted.length === 0) {\n return [];\n }\n // use non-wildcard word search from dc.publisher\n return [`dc.publisher=\"${formatted}\"`];\n }\n\n return [];\n\n function getPublisher(record) {\n //debugData(record);\n const [field] = record.get(/^26[04]$/u).filter(f => f.subfields.some(sf => sf.code === 'b')); // Filter removes copyright-only fields\n //debugData(field);\n\n if (field) {\n const publisherString = field.subfields\n .filter(({code}) => ['b'].includes(code))\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value)\n // In Melinda's index subfield separators are indexed as ' '\n .join(' ')\n .trim();\n return publisherString;\n }\n return false;\n }\n}\n\nexport function bibYear(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibYear');\n const debugData = debug.extend('data');\n debug(`Creating query for the publishing year`);\n\n const year = getYear(record);\n if (year) {\n return [`dc.date=\"${year}\"`];\n }\n return [];\n\n function getYear(record) {\n const [f008] = record.get(/^008$/u);\n if (!f008 || !f008.value || f008.value.length < 11) {\n debug('f008 missing/crappy');\n return false;\n }\n\n debugData(`f008: ${JSON.stringify(f008)}`);\n const {value} = f008;\n if (value === '||||' || value === ' ') { // Meaningless values\n return false;\n }\n return value.slice(7, 11);\n }\n}\n\nexport function checkUseWordSearch(formatted) {\n const lowercased = formatted.toLowerCase();\n // Note: add a space to startWords to catch just actual boolean words\n const booleanStartWords = ['and ', 'or ', 'nor ', 'not '];\n return booleanStartWords.some(word => lowercased.startsWith(word));\n}\n\nexport function bibStandardIdentifiers(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibStandardIdentifiers');\n const debugData = debug.extend('data');\n debug(`Creating queries for standard identifiers`);\n\n // DEVELOP: should we query also f015 and f028?\n\n // const fields = record.get(/^(?<def>020|022|024)$/u);\n const fields = record.get(/^(?<def>015|020|022|024|028)$/u);\n const identifiers = [].concat(...fields.map(toIdentifiers));\n const uniqueIdentifiers = [...new Set(identifiers)];\n\n debugData(`Standard identifier fields: ${JSON.stringify(fields)}`);\n debugData(`Identifiers (${identifiers.length}): ${JSON.stringify(identifiers)}`);\n debugData(`Unique identifiers (${uniqueIdentifiers.length}): ${JSON.stringify(uniqueIdentifiers)}`);\n\n if (uniqueIdentifiers.length < 1) {\n debug(`No identifiers found, no queries created.`);\n return [];\n }\n\n return toQueries(uniqueIdentifiers, 'dc.identifier');\n\n function toIdentifiers({tag, subfields}) {\n const issnIsbnReqExp = (/^[A-Za-z0-9-]+$/u);\n const otherIdReqExp = (/^[A-Za-z0-9-:]+$/u);\n\n if (tag === '015') { // TODO: test (does this use the right index etc.)\n return subfields\n .filter(sf => sf.code === 'a' && sf.value.match(/^[A-Z0-9\\-]+$/ui))\n .map(sf => sf.value)\n }\n\n if (tag === '020') {\n return subfields\n .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))\n .map(({value}) => String(value));\n }\n\n if (tag === '022') {\n return subfields\n .filter(sub => ['a', 'z', 'y'].includes(sub.code) && testStringOrNumber(sub.value) && issnIsbnReqExp.test(String(sub.value)))\n .map(({value}) => String(value));\n }\n\n if (tag === '028') { // TODO: test\n const a = subfields.find(sf => sf.code === 'a');\n if (a) {\n const b = subfields.find(sf => sf.code === 'b');\n // TODO: normalize?\n if (b) {\n return [`^${a.value}`, `^${b.value} ${a.value}`];\n }\n return [`^${a.value}`];\n }\n }\n\n // Default seems to be 024:\n return subfields\n .filter(sub => ['a', 'z'].includes(sub.code) && testStringOrNumber(sub.value) && otherIdReqExp.test(String(sub.value)))\n .map(({value}) => String(value));\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["debug", "fSids", "sidStrings", "field", "title", "formatted", "query", "queryIsOkAlone", "addYear", "alternates", "alternateQueries", "newAlternateQueries", "record"]
|
|
7
7
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import yargs from "yargs";
|
|
|
3
3
|
import createMatchOperator, { candidateSearch, matchDetection } from "./index.js";
|
|
4
4
|
import createDebugLogger from "debug";
|
|
5
5
|
import { MarcRecord } from "@natlibfi/marc-record";
|
|
6
|
+
import { uniqArray } from "@natlibfi/marc-record-validators-melinda/dist/utils.js";
|
|
6
7
|
cli();
|
|
7
8
|
async function cli() {
|
|
8
9
|
const debug = createDebugLogger("@natlibfi/melinda-record-matching:cli");
|
|
@@ -44,6 +45,17 @@ async function cli() {
|
|
|
44
45
|
const fileRaw = fs.readFileSync(file, "utf8");
|
|
45
46
|
const record = new MarcRecord(JSON.parse(fileRaw), { subfieldValues: false });
|
|
46
47
|
const result = await matchOperator({ record });
|
|
48
|
+
const matchCount = result.matches.length;
|
|
49
|
+
const matchIds = result.matches.map((matchA) => matchA.candidate.id);
|
|
50
|
+
const nonMatchCount = result.nonMatches?.length || result.nonMatchCount;
|
|
51
|
+
const nonMatchIds = result.nonMatches?.map((matchA) => matchA.candidate.id) || [];
|
|
52
|
+
const uniqueNonMatchIds = uniqArray(nonMatchIds);
|
|
53
|
+
const uniqueNonMatchCount = uniqueNonMatchIds.length;
|
|
54
|
+
const candidateCount = result.candidateCount;
|
|
55
|
+
debug(`Matches (${matchCount}) ${JSON.stringify(matchIds)}`);
|
|
56
|
+
debug(`NonMatches (${nonMatchCount}) ${JSON.stringify(nonMatchIds)}`);
|
|
57
|
+
debug(`Unique nonMatches (${uniqueNonMatchCount}) ${JSON.stringify(uniqueNonMatchIds)}`);
|
|
58
|
+
debug(`Candidates (${candidateCount})`);
|
|
47
59
|
debug(JSON.stringify(result));
|
|
48
60
|
function generateStrategy(searchType2) {
|
|
49
61
|
if (["IDS"].includes(searchType2)) {
|
|
@@ -71,7 +83,9 @@ async function cli() {
|
|
|
71
83
|
// - ignore one year differences in publicationTime
|
|
72
84
|
// - extract publicationTimes from f008, f26x and reprint notes in f500
|
|
73
85
|
// - do not substract points for mismatching (normal) publicationTime, if there's a match between
|
|
74
|
-
// normal publicationTime and a
|
|
86
|
+
// normal publicationTime and a reprintPublicationTime
|
|
87
|
+
// - do not subract points for mismatchin (normal) publiscationTime if there's a match between
|
|
88
|
+
// normal publicationTime and a copyrightPublicationTime
|
|
75
89
|
matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),
|
|
76
90
|
matchDetection.features.bib.language(),
|
|
77
91
|
matchDetection.features.bib.bibliographicLevel(),
|
package/dist/cli.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/cli.js"],
|
|
4
|
-
"sourcesContent": ["import fs from 'fs';\nimport yargs from 'yargs';\nimport createMatchOperator, {candidateSearch, matchDetection} from './index.js';\nimport createDebugLogger from 'debug';\nimport {MarcRecord} from '@natlibfi/marc-record';\n\ncli();\n\n// eslint-disable-next-line max-lines-per-function\nasync function cli() {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:cli');\n const args = yargs(process.argv.slice(2))\n .scriptName('melinda-record-matching-js')\n .epilog('Copyright (C) 2022-2023 University Of Helsinki (The National Library Of Finland)')\n .usage('$0 <file> [options] and env variable info in README')\n .usage('Installed globally: $0 <file> [options] and env variable info in README')\n .usage('Not installed: npx $0 <file> [options] and env variable info in README')\n .usage('Build from source: node dist/index.js <file> [options] and env variable info in README')\n .showHelpOnFail(true)\n .example([['$ node /dist/cli.js record.json']])\n .env('MELINDA_RECORD_MATCH')\n .version()\n .positional('file', {type: 'string', describe: 'Json file of records to match'})\n .options({\n t: {type: 'string', default: 'IDS', alias: 'searchType', describe: 'IDS, STANDARD_IDS, COMPONENT, CONTENT or CONTENTALT'},\n m: {type: 'number', default: 1, alias: 'maxMatches', describe: ''},\n c: {type: 'number', default: 1000, alias: 'maxCandidates', describe: ''},\n s: {type: 'boolean', default: false, alias: 'returnStrategy', describe: ''},\n q: {type: 'boolean', default: false, alias: 'returnQuery', describe: ''},\n n: {type: 'boolean', default: false, alias: 'returnNonMatches', describe: ''}\n })\n .check((args) => {\n const [file] = args._;\n if (file === undefined) {\n throw new Error('No file argument given');\n }\n\n if (!fs.existsSync(file)) {\n throw new Error(`File ${file} does not exist`);\n }\n\n if (args.sruUrl === undefined) {\n throw new Error('Setup sru url');\n }\n\n if (!['IDS', 'STANDARD_IDS', 'COMPONENT', 'CONTENT', 'CONTENTALT', 'YSO'].includes(args.searchType)) {\n throw new Error('Invalid search type');\n }\n\n return true;\n })\n .parseSync();\n\n const [file] = args._;\n const {searchType} = args;\n debug(JSON.stringify(args));\n\n const detection = {\n threshold: 0.8999,\n strategy: generateStrategy(searchType)\n };\n\n const search = {\n url: args.sruUrl, searchSpec: generateSearchSpec(searchType)\n };\n\n const matchOperator = await createMatchOperator({detection, search, ...args});\n\n const fileRaw = fs.readFileSync(file, 'utf8');\n const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});\n\n const result = await matchOperator({record});\n debug(JSON.stringify(result));\n\n\n function generateStrategy(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.melindaId(),\n matchDetection.features.bib.allSourceIds(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n // We could have differing strategy for STANDARD_IDS\n // Let's not run title in strategy when we found the candidates through standard_ids search\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.publisherNumber(),\n // matchDetection.features.bib.sid(),\n matchDetection.features.bib.otherStandardIdentifier(),\n // Let's not use the same title matchDetection here\n //matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n // We probably should have some leeway here for notated music as BK etc.\n matchDetection.features.bib.recordType(),\n // Use publicationTimeAllowConsYearsMulti to\n // - ignore one year differences in publicationTime\n // - extract publicationTimes from f008, f26x and reprint notes in f500\n // - do not substract points for mismatching (normal) publicationTime, if there's a match between\n // normal publicationTime and a
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,WAAW;AAClB,OAAO,uBAAsB,iBAAiB,sBAAqB;AACnE,OAAO,uBAAuB;AAC9B,SAAQ,kBAAiB;
|
|
4
|
+
"sourcesContent": ["import fs from 'fs';\nimport yargs from 'yargs';\nimport createMatchOperator, {candidateSearch, matchDetection} from './index.js';\nimport createDebugLogger from 'debug';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';\n\ncli();\n\n// eslint-disable-next-line max-lines-per-function\nasync function cli() {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:cli');\n const args = yargs(process.argv.slice(2))\n .scriptName('melinda-record-matching-js')\n .epilog('Copyright (C) 2022-2023 University Of Helsinki (The National Library Of Finland)')\n .usage('$0 <file> [options] and env variable info in README')\n .usage('Installed globally: $0 <file> [options] and env variable info in README')\n .usage('Not installed: npx $0 <file> [options] and env variable info in README')\n .usage('Build from source: node dist/index.js <file> [options] and env variable info in README')\n .showHelpOnFail(true)\n .example([['$ node /dist/cli.js record.json']])\n .env('MELINDA_RECORD_MATCH')\n .version()\n .positional('file', {type: 'string', describe: 'Json file of records to match'})\n .options({\n t: {type: 'string', default: 'IDS', alias: 'searchType', describe: 'IDS, STANDARD_IDS, COMPONENT, CONTENT or CONTENTALT'},\n m: {type: 'number', default: 1, alias: 'maxMatches', describe: ''},\n c: {type: 'number', default: 1000, alias: 'maxCandidates', describe: ''},\n s: {type: 'boolean', default: false, alias: 'returnStrategy', describe: ''},\n q: {type: 'boolean', default: false, alias: 'returnQuery', describe: ''},\n n: {type: 'boolean', default: false, alias: 'returnNonMatches', describe: ''}\n })\n .check((args) => {\n const [file] = args._;\n if (file === undefined) {\n throw new Error('No file argument given');\n }\n\n if (!fs.existsSync(file)) {\n throw new Error(`File ${file} does not exist`);\n }\n\n if (args.sruUrl === undefined) {\n throw new Error('Setup sru url');\n }\n\n if (!['IDS', 'STANDARD_IDS', 'COMPONENT', 'CONTENT', 'CONTENTALT', 'YSO'].includes(args.searchType)) {\n throw new Error('Invalid search type');\n }\n\n return true;\n })\n .parseSync();\n\n const [file] = args._;\n const {searchType} = args;\n debug(JSON.stringify(args));\n\n const detection = {\n threshold: 0.8999,\n strategy: generateStrategy(searchType)\n };\n\n const search = {\n url: args.sruUrl, searchSpec: generateSearchSpec(searchType)\n };\n\n const matchOperator = await createMatchOperator({detection, search, ...args});\n\n const fileRaw = fs.readFileSync(file, 'utf8');\n const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});\n\n const result = await matchOperator({record});\n const matchCount = result.matches.length;\n const matchIds = result.matches.map(matchA => matchA.candidate.id);\n const nonMatchCount = result.nonMatches?.length || result.nonMatchCount;\n const nonMatchIds = result.nonMatches?.map(matchA => matchA.candidate.id) || [];\n const uniqueNonMatchIds = uniqArray(nonMatchIds);\n const uniqueNonMatchCount = uniqueNonMatchIds.length;\n const candidateCount = result.candidateCount;\n\n debug(`Matches (${matchCount}) ${JSON.stringify(matchIds)}`);\n debug(`NonMatches (${nonMatchCount}) ${JSON.stringify(nonMatchIds)}`);\n debug(`Unique nonMatches (${uniqueNonMatchCount}) ${JSON.stringify(uniqueNonMatchIds)}`);\n debug(`Candidates (${candidateCount})`);\n\n debug(JSON.stringify(result));\n\n // DEVELOP: print results to output file\n\n function generateStrategy(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.melindaId(),\n matchDetection.features.bib.allSourceIds(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n // We could have differing strategy for STANDARD_IDS\n // Let's not run title in strategy when we found the candidates through standard_ids search\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.publisherNumber(),\n // matchDetection.features.bib.sid(),\n matchDetection.features.bib.otherStandardIdentifier(),\n // Let's not use the same title matchDetection here\n //matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n // We probably should have some leeway here for notated music as BK etc.\n matchDetection.features.bib.recordType(),\n // Use publicationTimeAllowConsYearsMulti to\n // - ignore one year differences in publicationTime\n // - extract publicationTimes from f008, f26x and reprint notes in f500\n // - do not substract points for mismatching (normal) publicationTime, if there's a match between\n // normal publicationTime and a reprintPublicationTime\n // - do not subract points for mismatchin (normal) publiscationTime if there's a match between \n // normal publicationTime and a copyrightPublicationTime\n matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.bibliographicLevel(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n if (['COMPONENT'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.otherStandardIdentifier(),\n matchDetection.features.bib.recordType(),\n matchDetection.features.bib.title(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.authors(),\n matchDetection.features.bib.bibliographicLevel(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n if (['CONTENT', 'CONTENTALT'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.publisherNumber(),\n // matchDetection.features.bib.sid(),\n matchDetection.features.bib.otherStandardIdentifier(),\n matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n matchDetection.features.bib.recordType(),\n matchDetection.features.bib.publicationTime(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.bibliographicLevel(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n if (['YSO']) {\n return [\n // matchDetection.features.bib.sid(), // SIDs might be relevant on the auth side as well?\n matchDetection.features.auth.yso()\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n\n\n function generateSearchSpec(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.melindaId,\n candidateSearch.searchTypes.bib.sourceIds\n ];\n }\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [candidateSearch.searchTypes.bib.standardIdentifiers];\n }\n\n if (['COMPONENT'].includes(searchType)) {\n return [\n //candidateSearch.searchTypes.bib.sourceIds,\n candidateSearch.searchTypes.component.hostIdMelinda,\n candidateSearch.searchTypes.component.hostIdOtherSource\n ];\n }\n\n if (['CONTENT'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.hostComponents,\n //candidateSearch.searchTypes.bib.titleAuthor,\n candidateSearch.searchTypes.bib.title\n ];\n }\n if (['CONTENTALT'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.hostComponents,\n // titleAuthorYearAlternates searches for matchCandidates\n // with alternate queries, starting from more tight searches\n candidateSearch.searchTypes.bib.titleAuthorYearAlternates\n ];\n }\n\n if (searchType === 'YSO') {\n return [\n candidateSearch.searchTypes.auth.authURX\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["args", "file", "searchType"]
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { candidateSearch, matchDetection };
|
|
|
5
5
|
export default ({ detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false }) => {
|
|
6
6
|
const debug = createDebugLogger("@natlibfi/melinda-record-matching:index");
|
|
7
7
|
const debugData = debug.extend("data");
|
|
8
|
+
const debugDev = debug.extend("dev");
|
|
8
9
|
debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);
|
|
9
10
|
debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);
|
|
10
11
|
debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);
|
|
@@ -20,7 +21,7 @@ export default ({ detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
20
21
|
async function iterate({ initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = [] }) {
|
|
21
22
|
debugData(`Starting next matcher iteration.`);
|
|
22
23
|
const { records, ...state } = await search(initialState);
|
|
23
|
-
|
|
24
|
+
debugDev(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, duplicateCount: ${duplicateCount}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);
|
|
24
25
|
const recordSetSize = records.length;
|
|
25
26
|
const newCandidateCount = candidateCount + recordSetSize;
|
|
26
27
|
if (recordSetSize > 0) {
|
|
@@ -34,7 +35,7 @@ export default ({ detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
34
35
|
return returnResult({ matches, state, stopReason: "", nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors });
|
|
35
36
|
function handleRecordSet() {
|
|
36
37
|
debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
|
|
37
|
-
const matchResult = iterateRecords({ records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount });
|
|
38
|
+
const matchResult = iterateRecords({ records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount, duplicateCount });
|
|
38
39
|
const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
|
|
39
40
|
const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
|
|
40
41
|
const { newMatches, newNonMatches, newMatchErrors } = handleMatchResult(matchResult, matches, nonMatches, matchErrors);
|
|
@@ -122,8 +123,9 @@ export default ({ detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
122
123
|
const [candidate] = records;
|
|
123
124
|
const newRecordCount = candidate ? recordCount + 1 : recordCount;
|
|
124
125
|
if (candidate) {
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
const { record: candidateRecord, id: candidateId } = candidate;
|
|
127
|
+
const allMatches = matches.concat(nonMatches);
|
|
128
|
+
if (candidateNotInMatches(allMatches, candidate)) {
|
|
127
129
|
const recordBExternal = { id: candidate.id, recordSource: "databaseRecord", label: `db-${candidate.id}` };
|
|
128
130
|
try {
|
|
129
131
|
debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);
|
|
@@ -136,7 +138,9 @@ export default ({ detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
136
138
|
return iterateRecords({ records: records.slice(1), recordSetSize, maxMatches: maxMatches2, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors });
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
|
-
|
|
141
|
+
debug(`Matching for ${candidateId} already done (${recordDuplicateCount} before)`);
|
|
142
|
+
const newRecordDuplicateCount = recordDuplicateCount + 1;
|
|
143
|
+
return iterateRecords({ records: records.slice(1), recordSetSize, maxMatches: maxMatches2, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: newRecordDuplicateCount, recordNonMatchCount, recordMatchErrors });
|
|
140
144
|
}
|
|
141
145
|
debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);
|
|
142
146
|
return { matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount, matchErrors: recordMatchErrors };
|
|
@@ -168,28 +172,27 @@ export default ({ detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
168
172
|
const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;
|
|
169
173
|
const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
|
|
170
174
|
const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;
|
|
171
|
-
|
|
175
|
+
debugDev(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches2})`);
|
|
172
176
|
if (returnNonMatches) {
|
|
173
|
-
|
|
177
|
+
debugDev(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
|
|
174
178
|
}
|
|
175
|
-
|
|
179
|
+
debugDev(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);
|
|
176
180
|
if (maxMatchesFound({ matches: matches.concat(newRecordMatches), maxMatches: maxMatches2 })) {
|
|
177
181
|
debug(`MaxMatches (${maxMatches2}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);
|
|
178
182
|
return { matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors };
|
|
179
183
|
}
|
|
180
|
-
return iterateRecords({ records: records.slice(1), recordSetSize, maxMatches: maxMatches2, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [],
|
|
184
|
+
return iterateRecords({ records: records.slice(1), recordSetSize, maxMatches: maxMatches2, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors });
|
|
181
185
|
}
|
|
182
|
-
function candidateNotInMatches(
|
|
183
|
-
debug(`Checking that record ${candidate2.id} is not already included in ${
|
|
186
|
+
function candidateNotInMatches(allMatches, candidate2) {
|
|
187
|
+
debug(`Checking that record ${candidate2.id} is not already included in ${allMatches.length} matches/nonMatches`);
|
|
184
188
|
const newCandidateId = candidate2.id;
|
|
185
|
-
debugData(
|
|
186
|
-
const result =
|
|
187
|
-
debugData(`Result: ${result}`);
|
|
189
|
+
debugData(`-- newCandidateId: ${newCandidateId}`);
|
|
190
|
+
const result = allMatches.find(({ candidate: candidate3 }) => candidate3.id === newCandidateId);
|
|
188
191
|
if (result) {
|
|
189
|
-
debug(
|
|
192
|
+
debug(`-- ${newCandidateId} was already handled.`);
|
|
190
193
|
return false;
|
|
191
194
|
}
|
|
192
|
-
|
|
195
|
+
debugDev(`-- ${newCandidateId} not found in ${allMatches.length} matches/nonMatches`);
|
|
193
196
|
return true;
|
|
194
197
|
}
|
|
195
198
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.js"],
|
|
4
|
-
"sourcesContent": ["import createDebugLogger from 'debug';\nimport createSearchInterface, * as candidateSearch from './candidate-search/index.js';\nimport createDetectionInterface, * as matchDetection from './match-detection/index.js';\n//import inspect from 'util';\n\nexport {candidateSearch, matchDetection};\n\nexport default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');\n const debugData = debug.extend('data');\n\n debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);\n debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);\n debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);\n debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);\n debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);\n debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);\n\n\n const detect = createDetectionInterface(detectionOptions, returnStrategy);\n\n return prepareSearch;\n\n async function prepareSearch({record, recordExternal = {recordSource: 'incomingRecord', label: 'ic'}}) {\n\n const {search} = await createSearchInterface({...searchOptions, record, maxCandidates, recordExternal});\n return iterate({});\n\n // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set\n // matches : candidates that have been detected as matches by current matcher job\n // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')\n // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // state.queryCandidateCounter: amount of candidate records retrieved from SRU for matching for current query, including the current record set (undefined if there were no queries left)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = []}) {\n debugData(`Starting next matcher iteration.`);\n const {records, ...state} = await search(initialState);\n\n debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);\n const recordSetSize = records.length;\n const newCandidateCount = candidateCount + recordSetSize;\n\n\n if (recordSetSize > 0) {\n return handleRecordSet();\n }\n\n if (state.queriesLeft > 0) {\n debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);\n return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, matchErrors});\n }\n\n debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);\n return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors});\n\n function handleRecordSet() {\n debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);\n\n const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount});\n\n const newDuplicateCount = duplicateCount + matchResult.duplicateCount;\n const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;\n const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);\n\n if (maxMatchesFound({matches: newMatches, maxMatches})) {\n return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {\n return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {\n debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);\n if (returnNonMatches) {\n debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);\n }\n\n const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);\n const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];\n const newMatchErrors = matchErrors.concat(matchResult.matchErrors);\n\n debugData(`- Total amount of matches: ${newMatches.length}`);\n if (returnNonMatches) {\n debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);\n }\n\n debugData(`MatchResult: ${JSON.stringify(matchResult)}`);\n debugData(`Old matchErrors: ${JSON.stringify(matchErrors)}, matchErrors from matchResult: ${JSON.stringify(matchResult.matchErrors)}, New matchErrors: ${JSON.stringify(newMatchErrors)}`);\n\n debugData(`- Total amount of matchErrors: ${newMatchErrors.length}`);\n\n return {newMatches, newNonMatches, newMatchErrors};\n }\n\n function addQuery(matches) {\n debugData(`Adding query ${state.query} to matches`);\n return matches.map((match) => ({...match, matchQuery: state.query}));\n }\n\n function maxCandidatesRetrieved(candidateCount, maxCandidates) {\n debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);\n if (maxCandidates && candidateCount >= maxCandidates) {\n debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);\n return true;\n }\n }\n }\n\n // matches : array of matching candidate records\n // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)\n // - candidate.id\n // - candidate.record\n // - probability\n // - strategy (if returnStrategy option is true)\n // - threshold (if returnStrategy option is true)\n // - matchQuery (if returnQuery option is true)\n // failures: array of conversionFailures from candidate-search and matchErrors from matchDetection in error format {status, payload: {message, id}} if returnFailures is true\n\n // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data\n\n // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)\n // - currently stopReason can be non-empty also in cases where status is true, if matcher hit the stop reason when handling the last available candidate record\n\n function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, matchErrors}) {\n const matchErrorCount = matchErrors.length;\n checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount});\n const matchStatus = getMatchState(state, stopReason, matchErrorCount);\n // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered\n const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches, candidateCount} : {matches, matchStatus, candidateCount};\n const result = matchesResult;\n debugData(`${JSON.stringify(result)}`);\n return result;\n\n // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match\n\n function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount}) {\n const matchCount = matches.length;\n debugData(`Return nonMatches: ${returnNonMatches}`);\n const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;\n const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;\n debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);\n debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);\n if (totalHandled !== candidateCount) {\n debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);\n return;\n }\n return;\n }\n\n function getMatchState(state, stopReason, matchErrorCount) {\n debugData(`${JSON.stringify(state)}`);\n debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);\n debug(`Queries left ${state.queriesLeft}, Searches for current query left: ${state.resultSetOffset && state.resultSetOffset <= state.totalRecords}, non-retrieved records: ${state.totalRecords - state.queryCandidateCounter}, maxedQueries (${state.maxedQueries.length}): ${state.maxedQueries}`);\n\n debugData(`StopReason: <${stopReason}>`);\n\n const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;\n const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;\n debugData(`nonRetrieved: ${nonRetrieved}`);\n\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here\n\n if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || matchErrorCount > 0) {\n const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;\n const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;\n const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || matchErrorsStopReason : stopReason;\n debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);\n debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);\n debugData(`NewStopReason: <${newStopReason}>`);\n debug(`Match status: false`);\n return {status: false, stopReason: newStopReason};\n }\n\n debug(`Match status: true`);\n return {status: true, stopReason};\n }\n }\n\n // NOTES:\n // - we could optimize by creating the featureSet for the incoming record once and using it for all database/candidateRecords\n // - if creating the featureSet for the incoming record fails we have an unprocessable entity\n // - if creating the featureSet for a candidate record fails we could skip that candidate - but list the case as a detectionFailure, same as conversionFailures\n\n function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0, recordMatchErrors = []}) {\n\n // recordSetSize : total amount of records in the current record set\n // recordCount : amount of records from the current record set that have been handled\n // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped\n // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results\n // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)\n\n // records : non-handled records in the current record set\n // matches : found matches in the current matcher job\n // recordMatches : found matches in the current record set\n // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)\n // recordMatchErrors: errored matchDetection in the current record set\n\n const [candidate] = records;\n const newRecordCount = candidate ? recordCount + 1 : recordCount;\n\n // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same\n // Exceptions would happen if the candidate would have been updated in the database between candidate searches\n // Note that if returnNonMatches is false, matcher won't remember candidates that didn't match, so they will be matched again everytime they are retrieved by\n // different candidate search queries. Same candidate search query won't have duplicate records.\n\n /* We could optimize and detect all retrieved candidates at once\n const candidateRecords = records.map(record => record.record);\n const recordsIsArray = Array.isArray(candidateRecords);\n debug(`records is an array: ${recordsIsArray}`);\n const result = detect(record, candidateRecords);\n debugData(`${JSON.stringify(result)}`);\n */\n\n if (candidate) {\n if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {\n const {record: candidateRecord, id: candidateId} = candidate;\n const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};\n try {\n debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);\n // we should handle errors from detection somehow - ie. cases where either record or candidateRecord errors\n const detectionResult = detect({recordA: record, recordB: candidateRecord, recordAExternal: recordExternal, recordBExternal});\n\n return handleDetectionResult(detectionResult, candidateId, candidateRecord);\n } catch (error) {\n debug(`MatchDetection errored: database record ${candidateId}: ${error}`);\n\n const matchError = {status: 422, payload: {message: `Matching errored for database record ${candidateId}. ${error.message}.`, id: candidateId}};\n const newRecordMatchErrors = recordMatchErrors.concat(matchError);\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors});\n }\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount, recordMatchErrors});\n }\n\n debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);\n return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount, matchErrors: recordMatchErrors};\n\n function handleDetectionResult(detectionResult, candidateId, candidateRecord) {\n debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);\n\n if (detectionResult.match || returnNonMatches) {\n debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);\n debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Threshold: ${JSON.stringify(detectionResult.threshold)}`);\n\n const matchResult = {\n probability: Math.round(detectionResult.probability * 100)/100,\n candidate: {\n id: candidateId,\n record: candidateRecord\n }\n };\n const strategyResult = {\n strategy: detectionResult.strategy,\n threshold: detectionResult.threshold\n };\n const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};\n\n debugData(`${JSON.stringify(newMatch)}`);\n\n return handleRecordMatch(detectionResult.match, newMatch);\n }\n\n const newRecordNonMatchCount = recordNonMatchCount + 1;\n debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, recordMatchErrors});\n }\n\n function handleRecordMatch(isMatch, newMatch) {\n const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;\n const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);\n const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;\n\n debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);\n\n if (returnNonMatches) {\n debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);\n }\n debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);\n\n if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {\n debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);\n return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors};\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors});\n }\n\n function candidateNotInMatches(matches, candidate) {\n debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);\n const newCandidateId = candidate.id;\n debugData(`newCandidateId: ${newCandidateId}`);\n const result = matches.find(({candidate}) => candidate.id === newCandidateId);\n debugData(`Result: ${result}`);\n if (result) {\n debug(`${candidate.id} was already handled.`);\n return false;\n }\n debug(`${candidate.id} not found in matches/nonMatches`);\n return true;\n }\n }\n\n function maxMatchesFound({matches, maxMatches}) {\n if (maxMatches && matches.length >= maxMatches) {\n debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);\n return true;\n }\n }\n }\n};\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,uBAAuB;AAC9B,OAAO,4BAA4B,qBAAqB;AACxD,OAAO,+BAA+B,oBAAoB;AAG1D,SAAQ,iBAAiB;AAEzB,eAAe,CAAC,EAAC,WAAW,kBAAkB,QAAQ,eAAe,aAAa,GAAG,gBAAgB,IAAI,iBAAiB,OAAO,cAAc,OAAO,mBAAmB,MAAK,MAAM;AAClL,QAAM,QAAQ,kBAAkB,yCAAyC;AACzE,QAAM,YAAY,MAAM,OAAO,MAAM;
|
|
4
|
+
"sourcesContent": ["import createDebugLogger from 'debug';\nimport createSearchInterface, * as candidateSearch from './candidate-search/index.js';\nimport createDetectionInterface, * as matchDetection from './match-detection/index.js';\n//import inspect from 'util';\n\nexport {candidateSearch, matchDetection};\n\nexport default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');\n const debugData = debug.extend('data');\n const debugDev = debug.extend('dev');\n\n\n debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);\n debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);\n debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);\n debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);\n debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);\n debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);\n\n\n const detect = createDetectionInterface(detectionOptions, returnStrategy);\n\n return prepareSearch;\n\n async function prepareSearch({record, recordExternal = {recordSource: 'incomingRecord', label: 'ic'}}) {\n\n const {search} = await createSearchInterface({...searchOptions, record, maxCandidates, recordExternal});\n return iterate({});\n\n // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set\n // matches : candidates that have been detected as matches by current matcher job\n // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')\n // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches\n // nonMatchCount : amount of candidate records that were detexted as nonMatches by current mathcer job\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // state.queryCandidateCounter: amount of candidate records retrieved from SRU for matching for current query, including the current record set (undefined if there were no queries left)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = []}) {\n debugData(`Starting next matcher iteration.`);\n const {records, ...state} = await search(initialState);\n\n debugDev(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, duplicateCount: ${duplicateCount}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);\n const recordSetSize = records.length;\n const newCandidateCount = candidateCount + recordSetSize;\n\n if (recordSetSize > 0) {\n return handleRecordSet();\n }\n\n if (state.queriesLeft > 0) {\n debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);\n return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, matchErrors});\n }\n\n debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);\n return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors});\n\n function handleRecordSet() {\n debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);\n\n const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount, duplicateCount});\n\n const newDuplicateCount = duplicateCount + matchResult.duplicateCount;\n const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;\n const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);\n\n if (maxMatchesFound({matches: newMatches, maxMatches})) {\n return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {\n return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {\n debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);\n if (returnNonMatches) {\n debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);\n }\n\n const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);\n const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];\n const newMatchErrors = matchErrors.concat(matchResult.matchErrors);\n\n debugData(`- Total amount of matches: ${newMatches.length}`);\n if (returnNonMatches) {\n debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);\n }\n\n debugData(`MatchResult: ${JSON.stringify(matchResult)}`);\n debugData(`Old matchErrors: ${JSON.stringify(matchErrors)}, matchErrors from matchResult: ${JSON.stringify(matchResult.matchErrors)}, New matchErrors: ${JSON.stringify(newMatchErrors)}`);\n\n debugData(`- Total amount of matchErrors: ${newMatchErrors.length}`);\n\n return {newMatches, newNonMatches, newMatchErrors};\n }\n\n function addQuery(matches) {\n debugData(`Adding query ${state.query} to matches`);\n return matches.map((match) => ({...match, matchQuery: state.query}));\n }\n\n function maxCandidatesRetrieved(candidateCount, maxCandidates) {\n debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);\n if (maxCandidates && candidateCount >= maxCandidates) {\n debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);\n return true;\n }\n }\n }\n\n // matches : array of matching candidate records\n // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)\n // - candidate.id\n // - candidate.record\n // - probability\n // - strategy (if returnStrategy option is true)\n // - threshold (if returnStrategy option is true)\n // - matchQuery (if returnQuery option is true)\n // failures: array of conversionFailures from candidate-search and matchErrors from matchDetection in error format {status, payload: {message, id}} if returnFailures is true\n\n // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data\n\n // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)\n // - currently stopReason can be non-empty also in cases where status is true, if matcher hit the stop reason when handling the last available candidate record\n\n function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, matchErrors}) {\n const matchErrorCount = matchErrors.length;\n checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount});\n const matchStatus = getMatchState(state, stopReason, matchErrorCount);\n // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered\n const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches, candidateCount} : {matches, matchStatus, candidateCount};\n const result = matchesResult;\n debugData(`${JSON.stringify(result)}`);\n return result;\n\n // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match\n\n function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount}) {\n const matchCount = matches.length;\n debugData(`Return nonMatches: ${returnNonMatches}`);\n const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;\n const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;\n debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);\n debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);\n // NOTE: this counts something wrong!\n if (totalHandled !== candidateCount) {\n debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);\n return;\n }\n return;\n }\n\n function getMatchState(state, stopReason, matchErrorCount) {\n debugData(`${JSON.stringify(state)}`);\n debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);\n debug(`Queries left ${state.queriesLeft}, Searches for current query left: ${state.resultSetOffset && state.resultSetOffset <= state.totalRecords}, non-retrieved records: ${state.totalRecords - state.queryCandidateCounter}, maxedQueries (${state.maxedQueries.length}): ${state.maxedQueries}`);\n\n debugData(`StopReason: <${stopReason}>`);\n\n const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;\n const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;\n debugData(`nonRetrieved: ${nonRetrieved}`);\n\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here\n\n if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || matchErrorCount > 0) {\n const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;\n const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;\n const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || matchErrorsStopReason : stopReason;\n debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);\n debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);\n debugData(`NewStopReason: <${newStopReason}>`);\n debug(`Match status: false`);\n return {status: false, stopReason: newStopReason};\n }\n\n debug(`Match status: true`);\n return {status: true, stopReason};\n }\n }\n\n // NOTES:\n // - we could optimize by creating the featureSet for the incoming record once and using it for all database/candidateRecords\n // - if creating the featureSet for the incoming record fails we have an unprocessable entity\n // - if creating the featureSet for a candidate record fails we could skip that candidate - but list the case as a detectionFailure, same as conversionFailures\n\n function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0, recordMatchErrors = []}) {\n\n // recordSetSize : total amount of records in the current record set\n // recordCount : amount of records from the current record set that have been handled\n // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped\n // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results\n // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)\n\n // records : non-handled records in the current record set\n // matches : found matches in the current matcher job\n // nonMatches: found nonMatches in the current matcher job\n // recordMatches : found matches in the current record set\n // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)\n // recordMatchErrors: errored matchDetection in the current record set\n\n const [candidate] = records;\n const newRecordCount = candidate ? recordCount + 1 : recordCount;\n\n // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same\n // Exceptions would happen if the candidate would have been updated in the database between candidate searches\n // Note that if returnNonMatches is false, matcher won't remember candidates that didn't match, so they will be matched again everytime they are retrieved by\n // different candidate search queries. Same candidate search query won't have duplicate records.\n\n /* We could optimize and detect all retrieved candidates at once\n const candidateRecords = records.map(record => record.record);\n const recordsIsArray = Array.isArray(candidateRecords);\n debug(`records is an array: ${recordsIsArray}`);\n const result = detect(record, candidateRecords);\n debugData(`${JSON.stringify(result)}`);\n */\n\n if (candidate) {\n const {record: candidateRecord, id: candidateId} = candidate;\n const allMatches = matches.concat(nonMatches);\n if (candidateNotInMatches(allMatches, candidate)) {\n const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};\n try {\n debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);\n // we should handle errors from detection somehow - ie. cases where either record or candidateRecord errors\n const detectionResult = detect({recordA: record, recordB: candidateRecord, recordAExternal: recordExternal, recordBExternal});\n\n return handleDetectionResult(detectionResult, candidateId, candidateRecord);\n } catch (error) {\n debug(`MatchDetection errored: database record ${candidateId}: ${error}`);\n\n const matchError = {status: 422, payload: {message: `Matching errored for database record ${candidateId}. ${error.message}.`, id: candidateId}};\n const newRecordMatchErrors = recordMatchErrors.concat(matchError);\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors});\n }\n }\n debug(`Matching for ${candidateId} already done (${recordDuplicateCount} before)`);\n const newRecordDuplicateCount = recordDuplicateCount + 1;\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: newRecordDuplicateCount, recordNonMatchCount, recordMatchErrors});\n }\n\n debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);\n return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount, matchErrors: recordMatchErrors};\n\n function handleDetectionResult(detectionResult, candidateId, candidateRecord) {\n debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);\n\n if (detectionResult.match || returnNonMatches) {\n debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);\n debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Threshold: ${JSON.stringify(detectionResult.threshold)}`);\n\n const matchResult = {\n probability: Math.round(detectionResult.probability * 100)/100,\n candidate: {\n id: candidateId,\n record: candidateRecord\n }\n };\n const strategyResult = {\n strategy: detectionResult.strategy,\n threshold: detectionResult.threshold\n };\n const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};\n\n debugData(`${JSON.stringify(newMatch)}`);\n\n return handleRecordMatch(detectionResult.match, newMatch);\n }\n\n const newRecordNonMatchCount = recordNonMatchCount + 1;\n debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, recordMatchErrors});\n }\n\n function handleRecordMatch(isMatch, newMatch) {\n const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;\n const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);\n const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;\n\n debugDev(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);\n\n if (returnNonMatches) {\n debugDev(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);\n }\n debugDev(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);\n\n if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {\n debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);\n return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors};\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors});\n }\n\n function candidateNotInMatches(allMatches, candidate) {\n debug(`Checking that record ${candidate.id} is not already included in ${allMatches.length} matches/nonMatches`);\n const newCandidateId = candidate.id;\n debugData(`-- newCandidateId: ${newCandidateId}`);\n const result = allMatches.find(({candidate}) => candidate.id === newCandidateId);\n //debugData(`Result: ${JSON.stringify(result)}`);\n if (result) {\n debug(`-- ${newCandidateId} was already handled.`);\n return false;\n }\n debugDev(`-- ${newCandidateId} not found in ${allMatches.length} matches/nonMatches`);\n return true;\n }\n }\n\n function maxMatchesFound({matches, maxMatches}) {\n if (maxMatches && matches.length >= maxMatches) {\n debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);\n return true;\n }\n }\n }\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["matches", "nonMatches", "matchErrors", "candidateCount", "maxCandidates", "duplicateCount", "nonMatchCount", "matchErrorCount", "state", "stopReason", "maxMatches", "candidate"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/match-detection/features/bib/publication-time-allow-cons-years-multi.js"],
|
|
4
|
-
"sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport {testStringOrNumber} from '../../../matching-utils.js';\n\n// We should also get copyright time and copyright/publication times from 26x\n// We should also get publishing time type from f008\n// We should get reprint times from f500 $a \"Lis\u00E4painos/Lis\u00E4painokset:\"\n\nexport default () => ({\n name: 'Publication time, allow consequent years, years from multiple sources',\n extract: ({record, recordExternal}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/publication-time-allow-cons-years-multi');\n const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const f008Values = extractF008Values(record);\n debug(`${label}: f008: ${JSON.stringify(f008Values)}`);\n\n const f26xValues = extractF26xValues(record);\n debug(`${label}: f26x: ${JSON.stringify(f26xValues)}`);\n\n const f500Values = extractF500Years(record);\n debug(`${label}: f500: ${JSON.stringify(f500Values)}`);\n\n // We should get copyrightYear from f008Date2 to copyrightYears when f008YearType = 'r'\n // Is the original year (f008Date2) in f008YearType === 'r' comparable to copyrightYear?\n // We should handle unknown years (here or comparison?)\n // We should handle year ranges for continuing resources / collections\n\n const normalYears = [...new Set(f26xValues.normalYears.concat(f008Values.f008Date1).filter(value => value && value !== ' ' && value !== '||||'))].sort();\n const copyrightYears = [...new Set(f26xValues.copyrightYears)].sort();\n const reprintYears = [...new Set(f500Values)].sort();\n\n const combined = {normalYears, copyrightYears, reprintYears};\n\n debug(`Combined: ${JSON.stringify(combined)}`);\n\n return combined;\n\n function extractF008Values(record) {\n // Record should have only one f008 - in case of several, we handle the first one\n const value = record.get(/^008$/u)?.[0]?.value || undefined;\n if (value && testStringOrNumber(value)) {\n const f008Date1 = extractF008Date1(value);\n const f008Date2 = extractF008Date2(value);\n const f008YearType = extractF008YearType(value);\n return {f008Date1, f008Date2, f008YearType};\n }\n return {f008Date1: undefined, f008Date2: undefined, f008YearType: undefined};\n\n function extractF008Date1(value) {\n return String(value).slice(7, 11);\n }\n\n function extractF008Date2(value) {\n return String(value).slice(11, 15);\n }\n\n function extractF008YearType(value) {\n return String(value).slice(6, 7);\n }\n }\n\n function extractF26xValues(record) {\n const copyrightRegex = /^(?<copyrightPrefix>cop|cop.|c|\u00A9|p|\u2117)/u;\n\n const pubNormalSubFieldValues = extractPubNormalSubfieldValues(record, copyrightRegex);\n debug(`Normal years: ${JSON.stringify(pubNormalSubFieldValues)}`);\n\n const pubF264CopySubFieldValues = extractPubF264CopySubfieldValues(record);\n debug(`F264 copyright years: ${JSON.stringify(pubF264CopySubFieldValues)}`);\n\n const pubF260CopySubFieldValues = extractPubF260CopySubfieldValues(record, copyrightRegex);\n debug(`F260 copyright years: ${JSON.stringify(pubF260CopySubFieldValues)}`);\n\n return {normalYears: pubNormalSubFieldValues, copyrightYears: [...pubF260CopySubFieldValues, ...pubF264CopySubFieldValues]};\n\n function extractPubNormalSubfieldValues(record, copyrightRegex) {\n return record.get(/^26[04]$/u)\n .filter((field) => !(field.tag === '264' && field.ind2 === '4'))\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code && code === 'c')\n .filter(({value}) => value && !copyrightRegex.test(value))\n .map(({value}) => value)\n .map((value) => removeNonAlphaNumeric(value));\n }\n\n function extractPubF264CopySubfieldValues(record, copyrightRegex) {\n return record.get(/^264$/u)\n .filter((field) => field.ind2 === '4')\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code && code === 'c')\n .filter(({value}) => value)\n .map(({value}) => value)\n .map((value) => value.replace(copyrightRegex, ''))\n .map((value) => removeNonAlphaNumeric(value));\n }\n\n function extractPubF260CopySubfieldValues(record, copyrightRegex) {\n return record.get(/^260$/u)\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code && code === 'c')\n .filter(({value}) => value && copyrightRegex.test(value))\n .map(({value}) => value)\n .map((value) => value.replace(copyrightRegex, ''))\n .map((value) => removeNonAlphaNumeric(value));\n }\n\n function removeNonAlphaNumeric(value) {\n debug(`Cleaning: ${JSON.stringify(value)}`);\n const nonAlphaNumericRegex = /[^A-Za-z0-9]/ug;\n return value ? value.replace(nonAlphaNumericRegex, '') : value;\n }\n }\n\n function extractF500Years(record) {\n\n const reprintRegex = /(?<reprint>Lis\u00E4painokset:|Lis\u00E4painos:)/u;\n const reprintFieldContents = record.get(/^500$/u)\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code === 'a')\n .map(({value}) => value);\n //.filter(value => value.test(reprintRegex));\n\n debug(`f500 reprint field contents: ${JSON.stringify(reprintFieldContents)}`);\n\n const filteredF500 = reprintFieldContents.filter((content) => content && content.match(reprintRegex));\n\n debug(`f500 reprint field contents (filtered): ${JSON.stringify(filteredF500)}`);\n\n const reprintYears = extractReprintYears(filteredF500);\n\n return reprintYears;\n }\n\n function extractReprintYears(contents) {\n const yearRegex = /[0-9][0-9][0-9][0-9]/gu;\n const years = contents.map(content => content.match(yearRegex))\n .flat();\n debug(`${JSON.stringify(years)}`);\n const uniqYears = [...new Set(years)].sort();\n debug(`${JSON.stringify(uniqYears)}`);\n return uniqYears;\n }\n\n },\n compare: (a, b) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/publication-time-allow-cons-years-multi');\n debug(`Comparing ${JSON.stringify(a)} to ${JSON.stringify(b)}`);\n\n const [firstA] = a.normalYears ? a.normalYears : a;\n const [firstB] = b.normalYears ? b.normalYears : b;\n\n debug(`Comparing ${JSON.stringify(firstA)} to ${JSON.stringify(firstB)}`);\n\n if (firstA === firstB) {\n return 0.1;\n }\n\n // If either of years is a non string/number, values are not comparable\n if (!testStringOrNumber(firstA) || !testStringOrNumber(firstB)) {\n return 0;\n }\n\n const firstANumber = parseInt(firstA, 10);\n const firstBNumber = parseInt(firstB, 10);\n\n // Handle consequent years as a match\n // see publication-time for a version that does not handle consequent years as a match\n if (!(isNaN(firstANumber) || isNaN(firstBNumber)) &&\n (firstANumber + 1 === firstBNumber || firstANumber - 1 === firstBNumber)) {\n return 0.1;\n }\n\n // We should do something with copyrightYears, too\n\n // Do not give minus points if a normal publishing year is found in normal years\n const bNormalInANormal = a.normalYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));\n const aNormalInBNormal = b.normalYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));\n debug(`BNorm in ANorm: ${JSON.stringify(bNormalInANormal)}`);\n debug(`ANorm in BNorm: ${JSON.stringify(aNormalInBNormal)}`);\n\n if (bNormalInANormal > 0 || aNormalInBNormal > 0) {\n return 0;\n }\n\n // Do not give minus points if a normal publishing year is found in reprint years\n const bNormalInAReprint = a.reprintYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));\n const aNormalInBReprint = b.reprintYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));\n debug(`BNorm in AReprint: ${JSON.stringify(bNormalInAReprint)}`);\n debug(`ANorm in BReprint: ${JSON.stringify(aNormalInBReprint)}`);\n\n if (bNormalInAReprint > 0 || aNormalInBReprint > 0) {\n return 0;\n }\n\n // Do not give minus points if a normal publishing year is found in copyright years\n const bNormalInACopyright = a.copyrightYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));\n const aNormalInBCopyright= b.copyrightYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));\n debug(`BNorm in ACopyright: ${JSON.stringify(bNormalInACopyright)}`);\n debug(`ANorm in BCopyright: ${JSON.stringify(aNormalInBCopyright)}`);\n\n if (bNormalInACopyright > 0 || aNormalInBCopyright > 0) {\n return 0;\n }\n\n\n\n\n\n return -1;\n\n }\n});\n\n// https://www.loc.gov/marc/bibliographic/bd008.html\n// field 008\n// 06 - Type of date/Publication status\n// 07-10 - Date 1\n// 11-14 - Date 2\n//\n// 06 - Type of date/Publication status\n// b - No dates given; B.C. date involved\n// c - Continuing resource currently published\n// d - Continuing resource ceased publication\n// e - Detailed date\n// i - Inclusive dates of collection\n// k - Range of years of bulk of collection\n// m - Multiple dates\n// n - Dates unknown\n// p - Date of distribution/release/issue and production/recording session when different\n// q - Questionable date\n// r - Reprint/reissue date and original date\n// s - Single known date/probable date\n// t - Publication date and copyright date\n// u - Continuing resource status unknown\n// | - No attempt to code\n//\n// 07-10 - Date 1\n// 1-9 - Date digit\n// # - Date element is not applicable\n// u - Date element is totally or partially unknown\n// |||| - No attempt to code\n//\n// 11-14 - Date 2\n// 1-9 - Date digit\n// # - Date element is not applicable\n// u - Date element is totally or partially unknown\n// |||| - No attempt to code\n//\n\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport {testStringOrNumber} from '../../../matching-utils.js';\n\n// We should also get copyright time and copyright/publication times from 26x\n// We should also get publishing time type from f008\n// We should get reprint times from f500 $a \"Lis\u00E4painos/Lis\u00E4painokset:\"\n\nexport default () => ({\n name: 'Publication time, allow consequent years, years from multiple sources',\n extract: ({record, recordExternal}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/publication-time-allow-cons-years-multi');\n const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const f008Values = extractF008Values(record);\n debug(`${label}: f008: ${JSON.stringify(f008Values)}`);\n\n const f26xValues = extractF26xValues(record);\n debug(`${label}: f26x: ${JSON.stringify(f26xValues)}`);\n\n const f500Values = extractF500Years(record);\n debug(`${label}: f500: ${JSON.stringify(f500Values)}`);\n\n // We should get copyrightYear from f008Date2 to copyrightYears when f008YearType = 'r'\n // Is the original year (f008Date2) in f008YearType === 'r' comparable to copyrightYear?\n // We should handle unknown years (here or comparison?)\n // We should handle year ranges for continuing resources / collections\n\n const normalYears = [...new Set(f26xValues.normalYears.concat(f008Values.f008Date1).filter(value => value && value !== ' ' && value !== '||||'))].sort();\n const copyrightYears = [...new Set(f26xValues.copyrightYears)].sort();\n const reprintYears = [...new Set(f500Values)].sort();\n\n const combined = {normalYears, copyrightYears, reprintYears};\n\n debug(`Combined: ${JSON.stringify(combined)}`);\n\n return combined;\n\n function extractF008Values(record) {\n // Record should have only one f008 - in case of several, we handle the first one\n const value = record.get(/^008$/u)?.[0]?.value || undefined;\n if (value && testStringOrNumber(value)) {\n const f008Date1 = extractF008Date1(value);\n const f008Date2 = extractF008Date2(value);\n const f008YearType = extractF008YearType(value);\n return {f008Date1, f008Date2, f008YearType};\n }\n return {f008Date1: undefined, f008Date2: undefined, f008YearType: undefined};\n\n function extractF008Date1(value) {\n return String(value).slice(7, 11);\n }\n\n function extractF008Date2(value) {\n return String(value).slice(11, 15);\n }\n\n function extractF008YearType(value) {\n return String(value).slice(6, 7);\n }\n }\n\n function extractF26xValues(record) {\n const copyrightRegex = /^(?<copyrightPrefix>cop|cop.|c|\u00A9|p|\u2117)/u;\n\n const pubNormalSubFieldValues = extractPubNormalSubfieldValues(record, copyrightRegex);\n debug(`Normal years: ${JSON.stringify(pubNormalSubFieldValues)}`);\n\n const pubF264CopySubFieldValues = extractPubF264CopySubfieldValues(record);\n debug(`F264 copyright years: ${JSON.stringify(pubF264CopySubFieldValues)}`);\n\n const pubF260CopySubFieldValues = extractPubF260CopySubfieldValues(record, copyrightRegex);\n debug(`F260 copyright years: ${JSON.stringify(pubF260CopySubFieldValues)}`);\n\n return {normalYears: pubNormalSubFieldValues, copyrightYears: [...pubF260CopySubFieldValues, ...pubF264CopySubFieldValues]};\n\n function extractPubNormalSubfieldValues(record, copyrightRegex) {\n return record.get(/^26[04]$/u)\n .filter((field) => !(field.tag === '264' && field.ind2 === '4'))\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code && code === 'c')\n .filter(({value}) => value && !copyrightRegex.test(value))\n .map(({value}) => value)\n .map((value) => removeNonAlphaNumeric(value));\n }\n\n function extractPubF264CopySubfieldValues(record, copyrightRegex) {\n return record.get(/^264$/u)\n .filter((field) => field.ind2 === '4')\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code && code === 'c')\n .filter(({value}) => value)\n .map(({value}) => value)\n .map((value) => value.replace(copyrightRegex, ''))\n .map((value) => removeNonAlphaNumeric(value));\n }\n\n function extractPubF260CopySubfieldValues(record, copyrightRegex) {\n return record.get(/^260$/u)\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code && code === 'c')\n .filter(({value}) => value && copyrightRegex.test(value))\n .map(({value}) => value)\n .map((value) => value.replace(copyrightRegex, ''))\n .map((value) => removeNonAlphaNumeric(value));\n }\n\n function removeNonAlphaNumeric(value) {\n debug(`Cleaning: ${JSON.stringify(value)}`);\n const nonAlphaNumericRegex = /[^A-Za-z0-9]/ug;\n return value ? value.replace(nonAlphaNumericRegex, '') : value;\n }\n }\n\n function extractF500Years(record) {\n\n const reprintRegex = /(?<reprint>Lis\u00E4painokset:|Lis\u00E4painos:)/u;\n const reprintFieldContents = record.get(/^500$/u)\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code === 'a')\n .map(({value}) => value);\n //.filter(value => value.test(reprintRegex));\n\n debug(`f500 reprint field contents: ${JSON.stringify(reprintFieldContents)}`);\n\n const filteredF500 = reprintFieldContents.filter((content) => content && content.match(reprintRegex));\n\n debug(`f500 reprint field contents (filtered): ${JSON.stringify(filteredF500)}`);\n\n const reprintYears = extractReprintYears(filteredF500);\n\n return reprintYears;\n }\n\n function extractReprintYears(contents) {\n const yearRegex = /[0-9][0-9][0-9][0-9]/gu;\n const years = contents.map(content => content.match(yearRegex))\n .flat();\n debug(`${JSON.stringify(years)}`);\n const uniqYears = [...new Set(years)].sort();\n debug(`${JSON.stringify(uniqYears)}`);\n return uniqYears;\n }\n\n },\n compare: (a, b) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/publication-time-allow-cons-years-multi');\n debug(`Comparing ${JSON.stringify(a)} to ${JSON.stringify(b)}`);\n\n const [firstA] = a.normalYears ? a.normalYears : a;\n const [firstB] = b.normalYears ? b.normalYears : b;\n\n debug(`Comparing ${JSON.stringify(firstA)} to ${JSON.stringify(firstB)}`);\n\n if (firstA === firstB) {\n return 0.1;\n }\n\n // If either of years is a non string/number, values are not comparable\n if (!testStringOrNumber(firstA) || !testStringOrNumber(firstB)) {\n return 0;\n }\n\n const firstANumber = parseInt(firstA, 10);\n const firstBNumber = parseInt(firstB, 10);\n\n // Handle consequent years as a match\n // see publication-time for a version that does not handle consequent years as a match\n if (!(isNaN(firstANumber) || isNaN(firstBNumber)) &&\n (firstANumber + 1 === firstBNumber || firstANumber - 1 === firstBNumber)) {\n return 0.1;\n }\n\n // We should do something with copyrightYears, too\n\n // Do not give minus points if a normal publishing year is found in normal years\n const bNormalInANormal = a.normalYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));\n const aNormalInBNormal = b.normalYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));\n debug(`BNorm in ANorm: ${JSON.stringify(bNormalInANormal)}`);\n debug(`ANorm in BNorm: ${JSON.stringify(aNormalInBNormal)}`);\n\n if (bNormalInANormal > 0 || aNormalInBNormal > 0) {\n return 0;\n }\n\n // Do not give minus points if a normal publishing year is found in reprint years\n const bNormalInAReprint = a.reprintYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));\n const aNormalInBReprint = b.reprintYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));\n debug(`BNorm in AReprint: ${JSON.stringify(bNormalInAReprint)}`);\n debug(`ANorm in BReprint: ${JSON.stringify(aNormalInBReprint)}`);\n\n if (bNormalInAReprint > 0 || aNormalInBReprint > 0) {\n return 0;\n }\n\n // Do not give minus points if a normal publishing year is found in copyright years\n\n const bNormalInACopyright = a.copyrightYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));\n const aNormalInBCopyright= b.copyrightYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));\n debug(`BNorm in ACopyright: ${JSON.stringify(bNormalInACopyright)}`);\n debug(`ANorm in BCopyright: ${JSON.stringify(aNormalInBCopyright)}`);\n\n if (bNormalInACopyright > 0 || aNormalInBCopyright > 0) {\n return 0;\n }\n\n\n\n\n\n return -1;\n\n }\n});\n\n// https://www.loc.gov/marc/bibliographic/bd008.html\n// field 008\n// 06 - Type of date/Publication status\n// 07-10 - Date 1\n// 11-14 - Date 2\n//\n// 06 - Type of date/Publication status\n// b - No dates given; B.C. date involved\n// c - Continuing resource currently published\n// d - Continuing resource ceased publication\n// e - Detailed date\n// i - Inclusive dates of collection\n// k - Range of years of bulk of collection\n// m - Multiple dates\n// n - Dates unknown\n// p - Date of distribution/release/issue and production/recording session when different\n// q - Questionable date\n// r - Reprint/reissue date and original date\n// s - Single known date/probable date\n// t - Publication date and copyright date\n// u - Continuing resource status unknown\n// | - No attempt to code\n//\n// 07-10 - Date 1\n// 1-9 - Date digit\n// # - Date element is not applicable\n// u - Date element is totally or partially unknown\n// |||| - No attempt to code\n//\n// 11-14 - Date 2\n// 1-9 - Date digit\n// # - Date element is not applicable\n// u - Date element is totally or partially unknown\n// |||| - No attempt to code\n//\n\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,uBAAuB;AAC9B,SAAQ,0BAAyB;AAMjC,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC,EAAC,QAAQ,eAAc,MAAM;AACrC,UAAM,QAAQ,kBAAkB,wGAAwG;AACxI,UAAM,QAAQ,kBAAkB,eAAe,QAAQ,eAAe,QAAQ;AAE9E,UAAM,aAAa,kBAAkB,MAAM;AAC3C,UAAM,GAAG,KAAK,WAAW,KAAK,UAAU,UAAU,CAAC,EAAE;AAErD,UAAM,aAAa,kBAAkB,MAAM;AAC3C,UAAM,GAAG,KAAK,WAAW,KAAK,UAAU,UAAU,CAAC,EAAE;AAErD,UAAM,aAAa,iBAAiB,MAAM;AAC1C,UAAM,GAAG,KAAK,WAAW,KAAK,UAAU,UAAU,CAAC,EAAE;AAOrD,UAAM,cAAc,CAAC,GAAG,IAAI,IAAI,WAAW,YAAY,OAAO,WAAW,SAAS,EAAE,OAAO,WAAS,SAAS,UAAU,UAAU,UAAU,MAAM,CAAC,CAAC,EAAE,KAAK;AAC1J,UAAM,iBAAiB,CAAC,GAAG,IAAI,IAAI,WAAW,cAAc,CAAC,EAAE,KAAK;AACpE,UAAM,eAAe,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,EAAE,KAAK;AAEnD,UAAM,WAAW,EAAC,aAAa,gBAAgB,aAAY;AAE3D,UAAM,aAAa,KAAK,UAAU,QAAQ,CAAC,EAAE;AAE7C,WAAO;AAEP,aAAS,kBAAkBA,SAAQ;AAEjC,YAAM,QAAQA,QAAO,IAAI,QAAQ,IAAI,CAAC,GAAG,SAAS;AAClD,UAAI,SAAS,mBAAmB,KAAK,GAAG;AACtC,cAAM,YAAY,iBAAiB,KAAK;AACxC,cAAM,YAAY,iBAAiB,KAAK;AACxC,cAAM,eAAe,oBAAoB,KAAK;AAC9C,eAAO,EAAC,WAAW,WAAW,aAAY;AAAA,MAC5C;AACA,aAAO,EAAC,WAAW,QAAW,WAAW,QAAW,cAAc,OAAS;AAE3E,eAAS,iBAAiBC,QAAO;AAC/B,eAAO,OAAOA,MAAK,EAAE,MAAM,GAAG,EAAE;AAAA,MAClC;AAEA,eAAS,iBAAiBA,QAAO;AAC/B,eAAO,OAAOA,MAAK,EAAE,MAAM,IAAI,EAAE;AAAA,MACnC;AAEA,eAAS,oBAAoBA,QAAO;AAClC,eAAO,OAAOA,MAAK,EAAE,MAAM,GAAG,CAAC;AAAA,MACjC;AAAA,IACF;AAEA,aAAS,kBAAkBD,SAAQ;AACjC,YAAM,iBAAiB;AAEvB,YAAM,0BAA0B,+BAA+BA,SAAQ,cAAc;AACrF,YAAM,iBAAiB,KAAK,UAAU,uBAAuB,CAAC,EAAE;AAEhE,YAAM,4BAA4B,iCAAiCA,OAAM;AACzE,YAAM,yBAAyB,KAAK,UAAU,yBAAyB,CAAC,EAAE;AAE1E,YAAM,4BAA4B,iCAAiCA,SAAQ,cAAc;AACzF,YAAM,yBAAyB,KAAK,UAAU,yBAAyB,CAAC,EAAE;AAE1E,aAAO,EAAC,aAAa,yBAAyB,gBAAgB,CAAC,GAAG,2BAA2B,GAAG,yBAAyB,EAAC;AAE1H,eAAS,+BAA+BA,SAAQE,iBAAgB;AAC9D,eAAOF,QAAO,IAAI,WAAW,EAC1B,OAAO,CAAC,UAAU,EAAE,MAAM,QAAQ,SAAS,MAAM,SAAS,IAAI,EAC9D,IAAI,CAAC,EAAC,UAAS,MAAM,SAAS,EAC9B,KAAK,EACL,OAAO,CAAC,EAAC,KAAI,MAAM,QAAQ,SAAS,GAAG,EACvC,OAAO,CAAC,EAAC,MAAK,MAAM,SAAS,CAACE,gBAAe,KAAK,KAAK,CAAC,EACxD,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK,EACtB,IAAI,CAAC,UAAU,sBAAsB,KAAK,CAAC;AAAA,MAChD;AAEA,eAAS,iCAAiCF,SAAQE,iBAAgB;AAChE,eAAOF,QAAO,IAAI,QAAQ,EACvB,OAAO,CAAC,UAAU,MAAM,SAAS,GAAG,EACpC,IAAI,CAAC,EAAC,UAAS,MAAM,SAAS,EAC9B,KAAK,EACL,OAAO,CAAC,EAAC,KAAI,MAAM,QAAQ,SAAS,GAAG,EACvC,OAAO,CAAC,EAAC,MAAK,MAAM,KAAK,EACzB,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK,EACtB,IAAI,CAAC,UAAU,MAAM,QAAQE,iBAAgB,EAAE,CAAC,EAChD,IAAI,CAAC,UAAU,sBAAsB,KAAK,CAAC;AAAA,MAChD;AAEA,eAAS,iCAAiCF,SAAQE,iBAAgB;AAChE,eAAOF,QAAO,IAAI,QAAQ,EACvB,IAAI,CAAC,EAAC,UAAS,MAAM,SAAS,EAC9B,KAAK,EACL,OAAO,CAAC,EAAC,KAAI,MAAM,QAAQ,SAAS,GAAG,EACvC,OAAO,CAAC,EAAC,MAAK,MAAM,SAASE,gBAAe,KAAK,KAAK,CAAC,EACvD,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK,EACtB,IAAI,CAAC,UAAU,MAAM,QAAQA,iBAAgB,EAAE,CAAC,EAChD,IAAI,CAAC,UAAU,sBAAsB,KAAK,CAAC;AAAA,MAChD;AAEA,eAAS,sBAAsB,OAAO;AACpC,cAAM,aAAa,KAAK,UAAU,KAAK,CAAC,EAAE;AAC1C,cAAM,uBAAuB;AAC7B,eAAO,QAAQ,MAAM,QAAQ,sBAAsB,EAAE,IAAI;AAAA,MAC3D;AAAA,IACF;AAEA,aAAS,iBAAiBF,SAAQ;AAEhC,YAAM,eAAe;AACrB,YAAM,uBAAuBA,QAAO,IAAI,QAAQ,EAC7C,IAAI,CAAC,EAAC,UAAS,MAAM,SAAS,EAC9B,KAAK,EACL,OAAO,CAAC,EAAC,KAAI,MAAM,SAAS,GAAG,EAC/B,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK;AAGzB,YAAM,gCAAgC,KAAK,UAAU,oBAAoB,CAAC,EAAE;AAE5E,YAAM,eAAe,qBAAqB,OAAO,CAAC,YAAY,WAAW,QAAQ,MAAM,YAAY,CAAC;AAEpG,YAAM,2CAA2C,KAAK,UAAU,YAAY,CAAC,EAAE;AAE/E,YAAMG,gBAAe,oBAAoB,YAAY;AAErD,aAAOA;AAAA,IACT;AAEA,aAAS,oBAAoB,UAAU;AACrC,YAAM,YAAY;AAClB,YAAM,QAAQ,SAAS,IAAI,aAAW,QAAQ,MAAM,SAAS,CAAC,EAC3D,KAAK;AACR,YAAM,GAAG,KAAK,UAAU,KAAK,CAAC,EAAE;AAChC,YAAM,YAAY,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK;AAC3C,YAAM,GAAG,KAAK,UAAU,SAAS,CAAC,EAAE;AACpC,aAAO;AAAA,IACT;AAAA,EAEF;AAAA,EACA,SAAS,CAAC,GAAG,MAAM;AACjB,UAAM,QAAQ,kBAAkB,wGAAwG;AACxI,UAAM,aAAa,KAAK,UAAU,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,EAAE;AAE9D,UAAM,CAAC,MAAM,IAAI,EAAE,cAAc,EAAE,cAAc;AACjD,UAAM,CAAC,MAAM,IAAI,EAAE,cAAc,EAAE,cAAc;AAEjD,UAAM,aAAa,KAAK,UAAU,MAAM,CAAC,OAAO,KAAK,UAAU,MAAM,CAAC,EAAE;AAExE,QAAI,WAAW,QAAQ;AACrB,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,mBAAmB,MAAM,KAAK,CAAC,mBAAmB,MAAM,GAAG;AAC9D,aAAO;AAAA,IACT;AAEA,UAAM,eAAe,SAAS,QAAQ,EAAE;AACxC,UAAM,eAAe,SAAS,QAAQ,EAAE;AAIxC,QAAI,EAAE,MAAM,YAAY,KAAK,MAAM,YAAY,OAC5C,eAAe,MAAM,gBAAgB,eAAe,MAAM,eAAe;AAC1E,aAAO;AAAA,IACT;AAKA,UAAM,mBAAmB,EAAE,YAAY,OAAO,YAAU,EAAE,YAAY,KAAK,YAAU,WAAW,MAAM,CAAC;AACvG,UAAM,mBAAmB,EAAE,YAAY,OAAO,YAAU,EAAE,YAAY,KAAK,YAAU,WAAW,MAAM,CAAC;AACvG,UAAM,mBAAmB,KAAK,UAAU,gBAAgB,CAAC,EAAE;AAC3D,UAAM,mBAAmB,KAAK,UAAU,gBAAgB,CAAC,EAAE;AAE3D,QAAI,mBAAmB,KAAK,mBAAmB,GAAG;AAChD,aAAO;AAAA,IACT;AAGA,UAAM,oBAAoB,EAAE,aAAa,OAAO,YAAU,EAAE,YAAY,KAAK,YAAU,WAAW,MAAM,CAAC;AACzG,UAAM,oBAAoB,EAAE,aAAa,OAAO,YAAU,EAAE,YAAY,KAAK,YAAU,WAAW,MAAM,CAAC;AACzG,UAAM,sBAAsB,KAAK,UAAU,iBAAiB,CAAC,EAAE;AAC/D,UAAM,sBAAsB,KAAK,UAAU,iBAAiB,CAAC,EAAE;AAE/D,QAAI,oBAAoB,KAAK,oBAAoB,GAAG;AAClD,aAAO;AAAA,IACT;AAIA,UAAM,sBAAsB,EAAE,eAAe,OAAO,YAAU,EAAE,YAAY,KAAK,YAAU,WAAW,MAAM,CAAC;AAC7G,UAAM,sBAAqB,EAAE,eAAe,OAAO,YAAU,EAAE,YAAY,KAAK,YAAU,WAAW,MAAM,CAAC;AAC5G,UAAM,wBAAwB,KAAK,UAAU,mBAAmB,CAAC,EAAE;AACnE,UAAM,wBAAwB,KAAK,UAAU,mBAAmB,CAAC,EAAE;AAEnE,QAAI,sBAAsB,KAAK,sBAAsB,GAAG;AACtD,aAAO;AAAA,IACT;AAMA,WAAO;AAAA,EAET;AACF;",
|
|
6
6
|
"names": ["record", "value", "copyrightRegex", "reprintYears"]
|
|
7
7
|
}
|
package/dist/matching-utils.js
CHANGED
|
@@ -91,8 +91,8 @@ export function stringBefore(string, substring) {
|
|
|
91
91
|
return string.substring(0, pos);
|
|
92
92
|
}
|
|
93
93
|
export function preferenceSortRecords(x, y) {
|
|
94
|
-
debug(`sortRecords: RECORD COMPARISON
|
|
95
|
-
|
|
94
|
+
debug(`sortRecords: RECORD COMPARISON`);
|
|
95
|
+
debugData(`{JSON.stringify(x)} vs
|
|
96
96
|
${JSON.stringify(y)}`);
|
|
97
97
|
const xFikka = hasLow(x.record, "FIKKA");
|
|
98
98
|
const yFikka = hasLow(y.record, "FIKKA");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/matching-utils.js"],
|
|
4
|
-
"sourcesContent": ["import createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:utils');\nconst debugData = debug.extend('data');\n\nexport function getMelindaIdsF035(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:melinda-id');\n const debugData = debug.extend('data');\n\n const f035s = record.getFields('035');\n\n if (f035s.length < 1) {\n debug(`No f035s found.`);\n return [];\n }\n\n const allF035MelindaIds = [].concat(...f035s.map(field => toMelindaIds(field)));\n const f035MelindaIds = [...new Set(allF035MelindaIds)];\n\n debugData(`Fields (${f035s.length}): ${JSON.stringify(f035s)}`);\n debugData(`Ids (${allF035MelindaIds.length}): ${JSON.stringify(allF035MelindaIds)}`);\n debugData(`Unique ids (${f035MelindaIds.length}): ${JSON.stringify(f035MelindaIds)}`);\n\n return f035MelindaIds;\n}\n\nexport function toMelindaIds({subfields}, subfieldsToParse = ['a', 'z']) {\n const melindaIdRegExp = /^(?<prefix>\\(FI-MELINDA\\)|FCC)(?<id>\\d{9})$/u;\n\n return subfields\n .filter(sub => subfieldsToParse.includes(sub.code))\n .filter(sub => testStringOrNumber(sub.value) && melindaIdRegExp.test(String(sub.value)))\n .map(({value}) => testStringOrNumber(value) ? String(value).replace(melindaIdRegExp, '$<id>') : '');\n}\n\nexport function validateSidFieldSubfieldCounts(field) {\n // Valid SID-fields have just one $c and one $b\n debugData(`Validating SID field ${JSON.stringify(field)}`);\n const countC = countSubfields(field, 'c');\n const countB = countSubfields(field, 'b');\n debug(`Found ${countC} sf $cs and ${countB} sf $bs. IsValid: ${countC === 1 && countB === 1}`);\n\n return countC === 1 && countB === 1;\n}\n\nfunction countSubfields(field, subfieldCode) {\n // debug(`Counting subfields ${subfieldCode}`);\n return field.subfields.filter(({code}) => code === subfieldCode).length;\n}\n\nexport function getSubfieldValues(field, subfieldCode) {\n debugData(`Get subfield(s) $${subfieldCode} from ${JSON.stringify(field)}`);\n return field.subfields\n .filter(({code}) => code === subfieldCode)\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value);\n}\n\nexport function testStringOrNumber(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return true;\n }\n return false;\n}\n\nexport function extractSubfieldsFromField(field, subfieldCodes) {\n if (field === undefined || field.subfields === undefined) {\n return [];\n }\n const resultSubfields = field.subfields\n .filter(({code}) => subfieldCodes.includes(code))\n .map(({code, value}) => ({code, value: testStringOrNumber(value) ? String(value) : ''}))\n .filter(value => value);\n return resultSubfields;\n}\n\nexport function uniqueSubfields(subfields) {\n return subfields.reduce((arr, e) => {\n if (!arr.find(item => item.code === e.code && item.value === e.value)) {\n const newArr = arr.concat(e);\n return newArr;\n }\n return arr;\n }, []);\n}\n\nexport function getMatchCounts(aValues, bValues) {\n\n const matchingValues = getMatchingValuesAmount(aValues, bValues);\n\n return {\n maxValues: aValues.length > bValues.length ? aValues.length : bValues.length,\n // possibleMatchingValues: amount of identifiers in set of less identifiers (we cannot more values than these)\n possibleMatchValues: aValues.length > bValues.length ? bValues.length : aValues.length,\n matchingValues\n };\n\n function getMatchingValuesAmount(aValues, bValues) {\n if (bValues.length > aValues.length) {\n return aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n }\n if (aValues.length > bValues.length) {\n return bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n }\n\n // If we have same amount of values, we'll check matches both ways, to avoid mixups in cases\n // there would be duplicate values\n const aToB = aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n const bToA = bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n\n return aToB < bToA ? aToB : bToA;\n }\n}\n\n\nexport function stringAfter(string, substring) {\n const pos = string.indexOf(substring);\n if (pos === -1) {\n return string;\n }\n return string.substring(pos+substring.length);\n}\n\nexport function stringBefore(string, substring) {\n const pos = string.indexOf(substring);\n if (pos === -1) {\n return string;\n }\n return string.substring(0, pos);\n}\n\n// We sort records here (put FIKKA and low-cased records first, ref MELINDA-3492)\n// NB! this only sorts the records the current set. If the best stuff would come in a later set and this set contains good enough record, we'll never reach the best...\n// Still this would alleviate the issue when maxMatches === 1...\nexport function preferenceSortRecords(x, y) {\n debug(`sortRecords: RECORD COMPARISON
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:utils');\nconst debugData = debug.extend('data');\n\nexport function getMelindaIdsF035(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:melinda-id');\n const debugData = debug.extend('data');\n\n const f035s = record.getFields('035');\n\n if (f035s.length < 1) {\n debug(`No f035s found.`);\n return [];\n }\n\n const allF035MelindaIds = [].concat(...f035s.map(field => toMelindaIds(field)));\n const f035MelindaIds = [...new Set(allF035MelindaIds)];\n\n debugData(`Fields (${f035s.length}): ${JSON.stringify(f035s)}`);\n debugData(`Ids (${allF035MelindaIds.length}): ${JSON.stringify(allF035MelindaIds)}`);\n debugData(`Unique ids (${f035MelindaIds.length}): ${JSON.stringify(f035MelindaIds)}`);\n\n return f035MelindaIds;\n}\n\nexport function toMelindaIds({subfields}, subfieldsToParse = ['a', 'z']) {\n const melindaIdRegExp = /^(?<prefix>\\(FI-MELINDA\\)|FCC)(?<id>\\d{9})$/u;\n\n return subfields\n .filter(sub => subfieldsToParse.includes(sub.code))\n .filter(sub => testStringOrNumber(sub.value) && melindaIdRegExp.test(String(sub.value)))\n .map(({value}) => testStringOrNumber(value) ? String(value).replace(melindaIdRegExp, '$<id>') : '');\n}\n\nexport function validateSidFieldSubfieldCounts(field) {\n // Valid SID-fields have just one $c and one $b\n debugData(`Validating SID field ${JSON.stringify(field)}`);\n const countC = countSubfields(field, 'c');\n const countB = countSubfields(field, 'b');\n debug(`Found ${countC} sf $cs and ${countB} sf $bs. IsValid: ${countC === 1 && countB === 1}`);\n\n return countC === 1 && countB === 1;\n}\n\nfunction countSubfields(field, subfieldCode) {\n // debug(`Counting subfields ${subfieldCode}`);\n return field.subfields.filter(({code}) => code === subfieldCode).length;\n}\n\nexport function getSubfieldValues(field, subfieldCode) {\n debugData(`Get subfield(s) $${subfieldCode} from ${JSON.stringify(field)}`);\n return field.subfields\n .filter(({code}) => code === subfieldCode)\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value);\n}\n\nexport function testStringOrNumber(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return true;\n }\n return false;\n}\n\nexport function extractSubfieldsFromField(field, subfieldCodes) {\n if (field === undefined || field.subfields === undefined) {\n return [];\n }\n const resultSubfields = field.subfields\n .filter(({code}) => subfieldCodes.includes(code))\n .map(({code, value}) => ({code, value: testStringOrNumber(value) ? String(value) : ''}))\n .filter(value => value);\n return resultSubfields;\n}\n\nexport function uniqueSubfields(subfields) {\n return subfields.reduce((arr, e) => {\n if (!arr.find(item => item.code === e.code && item.value === e.value)) {\n const newArr = arr.concat(e);\n return newArr;\n }\n return arr;\n }, []);\n}\n\nexport function getMatchCounts(aValues, bValues) {\n\n const matchingValues = getMatchingValuesAmount(aValues, bValues);\n\n return {\n maxValues: aValues.length > bValues.length ? aValues.length : bValues.length,\n // possibleMatchingValues: amount of identifiers in set of less identifiers (we cannot more values than these)\n possibleMatchValues: aValues.length > bValues.length ? bValues.length : aValues.length,\n matchingValues\n };\n\n function getMatchingValuesAmount(aValues, bValues) {\n if (bValues.length > aValues.length) {\n return aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n }\n if (aValues.length > bValues.length) {\n return bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n }\n\n // If we have same amount of values, we'll check matches both ways, to avoid mixups in cases\n // there would be duplicate values\n const aToB = aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n const bToA = bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n\n return aToB < bToA ? aToB : bToA;\n }\n}\n\n\nexport function stringAfter(string, substring) {\n const pos = string.indexOf(substring);\n if (pos === -1) {\n return string;\n }\n return string.substring(pos+substring.length);\n}\n\nexport function stringBefore(string, substring) {\n const pos = string.indexOf(substring);\n if (pos === -1) {\n return string;\n }\n return string.substring(0, pos);\n}\n\n// We sort records here (put FIKKA and low-cased records first, ref MELINDA-3492)\n// NB! this only sorts the records the current set. If the best stuff would come in a later set and this set contains good enough record, we'll never reach the best...\n// Still this would alleviate the issue when maxMatches === 1...\nexport function preferenceSortRecords(x, y) {\n debug(`sortRecords: RECORD COMPARISON`);\n debugData(`{JSON.stringify(x)} vs\\n${JSON.stringify(y)}`);\n // Prefer the record with a FIKKA LOW tag:\n const xFikka = hasLow(x.record, 'FIKKA');\n const yFikka = hasLow(y.record, 'FIKKA');\n\n if (xFikka && !yFikka) {\n debug('sortRecords: prefer FIKKA');\n return -1;\n }\n if (yFikka && !xFikka) {\n return 1;\n }\n\n // Prefer downcased:\n const xAllCaps = hasAllCapsTitle(x.record);\n const yAllCaps = hasAllCapsTitle(y.record);\n\n if (!xAllCaps && yAllCaps) {\n debug('sortRecords: prefer non-all caps');\n return -1; // x is better\n }\n if (!yAllCaps && xAllCaps) {\n return 1; // y is better\n }\n // Default:\n return 0;\n\n function hasLow(record, lowTag) {\n return record.fields?.some(f => f.tag === 'LOW' && f.subfields?.some(sf => sf.code === 'a' && sf.value === lowTag));\n }\n\n function hasAllCapsTitle(record) {\n const f245 = record.fields?.find(f => f.tag === '245');\n if (!f245) {\n debugData(`WARNING: No f245 found for ALL CAPS check`);\n return false;\n }\n\n const title = f245.subfields.filter(sf => ['a', 'b', 'c'].includes(sf.code)).map(sf => sf.value).join(' ');\n if (title.match(/[A-Z]/u) && !title.match(/[a-z]/u)) {\n return true;\n }\n return false;\n }\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["debug", "debugData", "aValues", "bValues"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -6,13 +6,18 @@ export function toQueries(identifiers, queryString) {
|
|
|
6
6
|
const debugData = debug.extend('data');
|
|
7
7
|
|
|
8
8
|
// We quote the identifier, if it contains a slash! (Slash in non-quoted SRU-search breaks search.)
|
|
9
|
-
|
|
9
|
+
// We also quote the identifier, if it starts with caret (f028 searches fail without caret and quotes...)
|
|
10
|
+
const quotedIdentifiers = identifiers.map(identifier => identifier.match(/\//u) || identifier.match(/\^/u) ? `"${identifier}"` : `${identifier}`);
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
// We can't pair queries with starting caret and without (ie. left anchored queries with non-left anchored queries)
|
|
13
|
+
const caretPairs = toPairs(quotedIdentifiers.filter(identifier => identifier.match(/\^/u)));
|
|
14
|
+
const nonCaretPairs = toPairs(quotedIdentifiers.filter(identifier => !identifier.match(/\^/u)));
|
|
14
15
|
|
|
16
|
+
const pairs = nonCaretPairs.concat(caretPairs);
|
|
15
17
|
debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);
|
|
18
|
+
|
|
19
|
+
// Aleph supports only two queries with or -operator (This is not actually true)
|
|
20
|
+
const queries = pairs.map(([a, b]) => b ? `${queryString}=${a} or ${queryString}=${b}` : `${queryString}=${a}`);
|
|
16
21
|
debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);
|
|
17
22
|
|
|
18
23
|
return queries;
|
|
@@ -479,9 +479,9 @@ export function bibStandardIdentifiers(record) {
|
|
|
479
479
|
const b = subfields.find(sf => sf.code === 'b');
|
|
480
480
|
// TODO: normalize?
|
|
481
481
|
if (b) {
|
|
482
|
-
return [a.value
|
|
482
|
+
return [`^${a.value}`, `^${b.value} ${a.value}`];
|
|
483
483
|
}
|
|
484
|
-
return [a.value];
|
|
484
|
+
return [`^${a.value}`];
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import yargs from 'yargs';
|
|
|
3
3
|
import createMatchOperator, {candidateSearch, matchDetection} from './index.js';
|
|
4
4
|
import createDebugLogger from 'debug';
|
|
5
5
|
import {MarcRecord} from '@natlibfi/marc-record';
|
|
6
|
+
import {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';
|
|
6
7
|
|
|
7
8
|
cli();
|
|
8
9
|
|
|
@@ -70,8 +71,22 @@ async function cli() {
|
|
|
70
71
|
const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});
|
|
71
72
|
|
|
72
73
|
const result = await matchOperator({record});
|
|
74
|
+
const matchCount = result.matches.length;
|
|
75
|
+
const matchIds = result.matches.map(matchA => matchA.candidate.id);
|
|
76
|
+
const nonMatchCount = result.nonMatches?.length || result.nonMatchCount;
|
|
77
|
+
const nonMatchIds = result.nonMatches?.map(matchA => matchA.candidate.id) || [];
|
|
78
|
+
const uniqueNonMatchIds = uniqArray(nonMatchIds);
|
|
79
|
+
const uniqueNonMatchCount = uniqueNonMatchIds.length;
|
|
80
|
+
const candidateCount = result.candidateCount;
|
|
81
|
+
|
|
82
|
+
debug(`Matches (${matchCount}) ${JSON.stringify(matchIds)}`);
|
|
83
|
+
debug(`NonMatches (${nonMatchCount}) ${JSON.stringify(nonMatchIds)}`);
|
|
84
|
+
debug(`Unique nonMatches (${uniqueNonMatchCount}) ${JSON.stringify(uniqueNonMatchIds)}`);
|
|
85
|
+
debug(`Candidates (${candidateCount})`);
|
|
86
|
+
|
|
73
87
|
debug(JSON.stringify(result));
|
|
74
88
|
|
|
89
|
+
// DEVELOP: print results to output file
|
|
75
90
|
|
|
76
91
|
function generateStrategy(searchType) {
|
|
77
92
|
if (['IDS'].includes(searchType)) {
|
|
@@ -102,7 +117,9 @@ async function cli() {
|
|
|
102
117
|
// - ignore one year differences in publicationTime
|
|
103
118
|
// - extract publicationTimes from f008, f26x and reprint notes in f500
|
|
104
119
|
// - do not substract points for mismatching (normal) publicationTime, if there's a match between
|
|
105
|
-
// normal publicationTime and a
|
|
120
|
+
// normal publicationTime and a reprintPublicationTime
|
|
121
|
+
// - do not subract points for mismatchin (normal) publiscationTime if there's a match between
|
|
122
|
+
// normal publicationTime and a copyrightPublicationTime
|
|
106
123
|
matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),
|
|
107
124
|
matchDetection.features.bib.language(),
|
|
108
125
|
matchDetection.features.bib.bibliographicLevel(),
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export {candidateSearch, matchDetection};
|
|
|
8
8
|
export default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {
|
|
9
9
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');
|
|
10
10
|
const debugData = debug.extend('data');
|
|
11
|
+
const debugDev = debug.extend('dev');
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);
|
|
13
15
|
debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);
|
|
@@ -31,6 +33,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
31
33
|
// matches : candidates that have been detected as matches by current matcher job
|
|
32
34
|
// nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')
|
|
33
35
|
// duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches
|
|
36
|
+
// nonMatchCount : amount of candidate records that were detexted as nonMatches by current mathcer job
|
|
34
37
|
|
|
35
38
|
// state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
|
|
36
39
|
// state.query : current query (undefined if there was no queries left)
|
|
@@ -44,11 +47,10 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
44
47
|
debugData(`Starting next matcher iteration.`);
|
|
45
48
|
const {records, ...state} = await search(initialState);
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
debugDev(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, duplicateCount: ${duplicateCount}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);
|
|
48
51
|
const recordSetSize = records.length;
|
|
49
52
|
const newCandidateCount = candidateCount + recordSetSize;
|
|
50
53
|
|
|
51
|
-
|
|
52
54
|
if (recordSetSize > 0) {
|
|
53
55
|
return handleRecordSet();
|
|
54
56
|
}
|
|
@@ -64,7 +66,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
64
66
|
function handleRecordSet() {
|
|
65
67
|
debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);
|
|
66
68
|
|
|
67
|
-
const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount});
|
|
69
|
+
const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount, duplicateCount});
|
|
68
70
|
|
|
69
71
|
const newDuplicateCount = duplicateCount + matchResult.duplicateCount;
|
|
70
72
|
const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;
|
|
@@ -154,6 +156,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
154
156
|
const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;
|
|
155
157
|
debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);
|
|
156
158
|
debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);
|
|
159
|
+
// NOTE: this counts something wrong!
|
|
157
160
|
if (totalHandled !== candidateCount) {
|
|
158
161
|
debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);
|
|
159
162
|
return;
|
|
@@ -206,6 +209,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
206
209
|
|
|
207
210
|
// records : non-handled records in the current record set
|
|
208
211
|
// matches : found matches in the current matcher job
|
|
212
|
+
// nonMatches: found nonMatches in the current matcher job
|
|
209
213
|
// recordMatches : found matches in the current record set
|
|
210
214
|
// recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)
|
|
211
215
|
// recordMatchErrors: errored matchDetection in the current record set
|
|
@@ -227,8 +231,9 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
227
231
|
*/
|
|
228
232
|
|
|
229
233
|
if (candidate) {
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
const {record: candidateRecord, id: candidateId} = candidate;
|
|
235
|
+
const allMatches = matches.concat(nonMatches);
|
|
236
|
+
if (candidateNotInMatches(allMatches, candidate)) {
|
|
232
237
|
const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};
|
|
233
238
|
try {
|
|
234
239
|
debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);
|
|
@@ -244,8 +249,9 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
244
249
|
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors});
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
|
-
|
|
248
|
-
|
|
252
|
+
debug(`Matching for ${candidateId} already done (${recordDuplicateCount} before)`);
|
|
253
|
+
const newRecordDuplicateCount = recordDuplicateCount + 1;
|
|
254
|
+
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: newRecordDuplicateCount, recordNonMatchCount, recordMatchErrors});
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);
|
|
@@ -287,32 +293,32 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
|
|
|
287
293
|
const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);
|
|
288
294
|
const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;
|
|
289
295
|
|
|
290
|
-
|
|
296
|
+
debugDev(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);
|
|
291
297
|
|
|
292
298
|
if (returnNonMatches) {
|
|
293
|
-
|
|
299
|
+
debugDev(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);
|
|
294
300
|
}
|
|
295
|
-
|
|
301
|
+
debugDev(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);
|
|
296
302
|
|
|
297
303
|
if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {
|
|
298
304
|
debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);
|
|
299
305
|
return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors};
|
|
300
306
|
}
|
|
301
307
|
|
|
302
|
-
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [],
|
|
308
|
+
return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors});
|
|
303
309
|
}
|
|
304
310
|
|
|
305
|
-
function candidateNotInMatches(
|
|
306
|
-
debug(`Checking that record ${candidate.id} is not already included in ${
|
|
311
|
+
function candidateNotInMatches(allMatches, candidate) {
|
|
312
|
+
debug(`Checking that record ${candidate.id} is not already included in ${allMatches.length} matches/nonMatches`);
|
|
307
313
|
const newCandidateId = candidate.id;
|
|
308
|
-
debugData(
|
|
309
|
-
const result =
|
|
310
|
-
debugData(`Result: ${result}`);
|
|
314
|
+
debugData(`-- newCandidateId: ${newCandidateId}`);
|
|
315
|
+
const result = allMatches.find(({candidate}) => candidate.id === newCandidateId);
|
|
316
|
+
//debugData(`Result: ${JSON.stringify(result)}`);
|
|
311
317
|
if (result) {
|
|
312
|
-
debug(
|
|
318
|
+
debug(`-- ${newCandidateId} was already handled.`);
|
|
313
319
|
return false;
|
|
314
320
|
}
|
|
315
|
-
|
|
321
|
+
debugDev(`-- ${newCandidateId} not found in ${allMatches.length} matches/nonMatches`);
|
|
316
322
|
return true;
|
|
317
323
|
}
|
|
318
324
|
}
|
|
@@ -198,6 +198,7 @@ export default () => ({
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// Do not give minus points if a normal publishing year is found in copyright years
|
|
201
|
+
|
|
201
202
|
const bNormalInACopyright = a.copyrightYears.filter(aValue => b.normalYears.some(bValue => aValue === bValue));
|
|
202
203
|
const aNormalInBCopyright= b.copyrightYears.filter(bValue => a.normalYears.some(aValue => bValue === aValue));
|
|
203
204
|
debug(`BNorm in ACopyright: ${JSON.stringify(bNormalInACopyright)}`);
|
package/src/matching-utils.js
CHANGED
|
@@ -134,7 +134,8 @@ export function stringBefore(string, substring) {
|
|
|
134
134
|
// NB! this only sorts the records the current set. If the best stuff would come in a later set and this set contains good enough record, we'll never reach the best...
|
|
135
135
|
// Still this would alleviate the issue when maxMatches === 1...
|
|
136
136
|
export function preferenceSortRecords(x, y) {
|
|
137
|
-
debug(`sortRecords: RECORD COMPARISON
|
|
137
|
+
debug(`sortRecords: RECORD COMPARISON`);
|
|
138
|
+
debugData(`{JSON.stringify(x)} vs\n${JSON.stringify(y)}`);
|
|
138
139
|
// Prefer the record with a FIKKA LOW tag:
|
|
139
140
|
const xFikka = hasLow(x.record, 'FIKKA');
|
|
140
141
|
const yFikka = hasLow(y.record, 'FIKKA');
|