@natlibfi/melinda-record-matching 4.4.0-alpha.1 → 5.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/melinda-node-tests.yml +13 -9
- package/LICENSE.txt +21 -165
- package/README.md +2 -2
- package/dist/candidate-search/candidate-search-utils.js +6 -43
- package/dist/candidate-search/candidate-search-utils.js.map +7 -1
- package/dist/candidate-search/choose-queries.js +75 -0
- package/dist/candidate-search/choose-queries.js.map +7 -0
- package/dist/candidate-search/index.js +89 -225
- package/dist/candidate-search/index.js.map +7 -1
- package/dist/candidate-search/index.test.js +92 -0
- package/dist/candidate-search/index.test.js.map +7 -0
- package/dist/candidate-search/query-list/bib.js +141 -320
- package/dist/candidate-search/query-list/bib.js.map +7 -1
- package/dist/candidate-search/query-list/bib.test.js +34 -0
- package/dist/candidate-search/query-list/bib.test.js.map +7 -0
- package/dist/candidate-search/query-list/component.js +87 -0
- package/dist/candidate-search/query-list/component.js.map +7 -0
- package/dist/candidate-search/query-list/index.js +39 -52
- package/dist/candidate-search/query-list/index.js.map +7 -1
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.js +87 -462
- package/dist/index.js.map +7 -1
- package/dist/index.test.js +69 -0
- package/dist/index.test.js.map +7 -0
- package/dist/match-detection/features/bib/all-source-ids.js +20 -91
- package/dist/match-detection/features/bib/all-source-ids.js.map +7 -1
- package/dist/match-detection/features/bib/authors.js +20 -76
- package/dist/match-detection/features/bib/authors.js.map +7 -1
- package/dist/match-detection/features/bib/bibliographic-level.js +4 -40
- package/dist/match-detection/features/bib/bibliographic-level.js.map +7 -1
- package/dist/match-detection/features/bib/host-component.js +5 -41
- package/dist/match-detection/features/bib/host-component.js.map +7 -1
- package/dist/match-detection/features/bib/index.js +17 -119
- package/dist/match-detection/features/bib/index.js.map +7 -1
- package/dist/match-detection/features/bib/index.test.js +40 -0
- package/dist/match-detection/features/bib/index.test.js.map +7 -0
- package/dist/match-detection/features/bib/isbn.js +13 -69
- package/dist/match-detection/features/bib/isbn.js.map +7 -1
- package/dist/match-detection/features/bib/issn.js +5 -50
- package/dist/match-detection/features/bib/issn.js.map +7 -1
- package/dist/match-detection/features/bib/language.js +25 -86
- package/dist/match-detection/features/bib/language.js.map +7 -1
- package/dist/match-detection/features/bib/media-type.js +12 -64
- package/dist/match-detection/features/bib/media-type.js.map +7 -1
- package/dist/match-detection/features/bib/melinda-id.js +5 -47
- package/dist/match-detection/features/bib/melinda-id.js.map +7 -1
- package/dist/match-detection/features/bib/melinda-identifier-factory.js +16 -67
- package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +7 -1
- package/dist/match-detection/features/bib/other-standard-identifier.js +5 -51
- package/dist/match-detection/features/bib/other-standard-identifier.js.map +7 -1
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js +49 -189
- package/dist/match-detection/features/bib/publication-time-allow-cons-years-multi.js.map +7 -1
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +10 -53
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +7 -1
- package/dist/match-detection/features/bib/publication-time.js +8 -46
- package/dist/match-detection/features/bib/publication-time.js.map +7 -1
- package/dist/match-detection/features/bib/record-type.js +4 -43
- package/dist/match-detection/features/bib/record-type.js.map +7 -1
- package/dist/match-detection/features/bib/standard-identifier-factory.js +47 -163
- package/dist/match-detection/features/bib/standard-identifier-factory.js.map +7 -1
- package/dist/match-detection/features/bib/title-version-original.js +11 -53
- package/dist/match-detection/features/bib/title-version-original.js.map +7 -1
- package/dist/match-detection/features/bib/title.js +20 -72
- package/dist/match-detection/features/bib/title.js.map +7 -1
- package/dist/match-detection/features/index.js +3 -11
- package/dist/match-detection/features/index.js.map +7 -1
- package/dist/match-detection/index.js +35 -150
- package/dist/match-detection/index.js.map +7 -1
- package/dist/match-detection/index.test.js +44 -0
- package/dist/match-detection/index.test.js.map +7 -0
- package/dist/matching-utils.js +38 -102
- package/dist/matching-utils.js.map +7 -1
- package/eslint.config.js +52 -0
- package/example.env +4 -0
- package/package.json +29 -78
- package/src/candidate-search/candidate-search-utils.js +1 -28
- package/src/candidate-search/choose-queries.js +93 -0
- package/src/candidate-search/index.js +85 -119
- package/src/candidate-search/{index.spec.js → index.test.js} +30 -43
- package/src/candidate-search/query-list/bib.js +54 -55
- package/src/candidate-search/query-list/bib.test.js +38 -0
- package/src/candidate-search/query-list/component.js +127 -0
- package/src/candidate-search/query-list/index.js +31 -35
- package/src/cli.js +177 -0
- package/src/index.js +27 -68
- package/src/{index.spec.js → index.test.js} +11 -42
- package/src/match-detection/features/bib/all-source-ids.js +2 -29
- package/src/match-detection/features/bib/authors.js +5 -30
- package/src/match-detection/features/bib/bibliographic-level.js +0 -27
- package/src/match-detection/features/bib/host-component.js +0 -27
- package/src/match-detection/features/bib/index.js +16 -43
- package/src/match-detection/features/bib/index.test.js +52 -0
- package/src/match-detection/features/bib/isbn.js +2 -29
- package/src/match-detection/features/bib/issn.js +1 -28
- package/src/match-detection/features/bib/language.js +5 -32
- package/src/match-detection/features/bib/media-type.js +2 -28
- package/src/match-detection/features/bib/melinda-id.js +1 -28
- package/src/match-detection/features/bib/melinda-identifier-factory.js +5 -32
- package/src/match-detection/features/bib/other-standard-identifier.js +1 -28
- package/src/match-detection/features/bib/publication-time-allow-cons-years-multi.js +7 -34
- package/src/match-detection/features/bib/publication-time-allow-cons-years.js +1 -28
- package/src/match-detection/features/bib/publication-time.js +2 -28
- package/src/match-detection/features/bib/record-type.js +0 -27
- package/src/match-detection/features/bib/standard-identifier-factory.js +1 -28
- package/src/match-detection/features/bib/title-version-original.js +2 -28
- package/src/match-detection/features/bib/title.js +6 -34
- package/src/match-detection/features/index.js +1 -28
- package/src/match-detection/index.js +1 -28
- package/src/match-detection/{index.spec.js → index.test.js} +9 -35
- package/src/matching-utils.js +8 -37
- package/dist/candidate-search/index.spec.js +0 -144
- package/dist/candidate-search/index.spec.js.map +0 -1
- package/dist/candidate-search/query-list/bib.spec.js +0 -64
- package/dist/candidate-search/query-list/bib.spec.js.map +0 -1
- package/dist/index.spec.js +0 -127
- package/dist/index.spec.js.map +0 -1
- package/dist/match-detection/features/bib/index.spec.js +0 -93
- package/dist/match-detection/features/bib/index.spec.js.map +0 -1
- package/dist/match-detection/index.spec.js +0 -91
- package/dist/match-detection/index.spec.js.map +0 -1
- package/src/candidate-search/query-list/bib.spec.js +0 -54
- package/src/match-detection/features/bib/index.spec.js +0 -78
package/eslint.config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Eslint configuration object for src
|
|
2
|
+
const configSrc = {
|
|
3
|
+
files: [
|
|
4
|
+
"src/*"
|
|
5
|
+
],
|
|
6
|
+
linterOptions: {
|
|
7
|
+
reportUnusedDisableDirectives: true,
|
|
8
|
+
},
|
|
9
|
+
rules: {
|
|
10
|
+
"no-console": "warn",
|
|
11
|
+
"eqeqeq": ["error", "always"],
|
|
12
|
+
"no-const-assign": "error",
|
|
13
|
+
"max-depth": ["warn", 4],
|
|
14
|
+
"max-lines": ["warn", 500],
|
|
15
|
+
"max-lines-per-function": ["warn", {"max": 100}],
|
|
16
|
+
"no-else-return": ["error", {allowElseIf: false}],
|
|
17
|
+
"no-plusplus": [
|
|
18
|
+
"error",
|
|
19
|
+
{
|
|
20
|
+
"allowForLoopAfterthoughts": true
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"array-callback-return": [
|
|
24
|
+
"error",
|
|
25
|
+
{
|
|
26
|
+
"checkForEach": false
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"no-unused-vars": [
|
|
30
|
+
"error",
|
|
31
|
+
{
|
|
32
|
+
"argsIgnorePattern": "next"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"no-warning-comments": "off"
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Eslint configuration object for globally ignoring .js files
|
|
40
|
+
// - ignore all files that start with a dot
|
|
41
|
+
// - ignore all files inside directories named 'dist'
|
|
42
|
+
const configIgnores = {
|
|
43
|
+
ignores: [
|
|
44
|
+
"**/.*",
|
|
45
|
+
"**/dist/"
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default [
|
|
50
|
+
configSrc,
|
|
51
|
+
configIgnores
|
|
52
|
+
];
|
package/example.env
ADDED
package/package.json
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git@github.com:natlibfi/melinda-record-matching-js.git"
|
|
15
15
|
},
|
|
16
|
-
"license": "
|
|
17
|
-
"version": "
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"version": "5.0.0-alpha.2",
|
|
18
18
|
"main": "./dist/index.js",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": "./dist/cli.js",
|
|
19
21
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
22
|
+
"node": ">=22"
|
|
21
23
|
},
|
|
22
24
|
"publishConfig": {
|
|
23
25
|
"access": "public"
|
|
@@ -25,86 +27,35 @@
|
|
|
25
27
|
"scripts": {
|
|
26
28
|
"prepare": "npm run build",
|
|
27
29
|
"start": "node dist/index.js",
|
|
30
|
+
"build": "esbuild \"src/**/*.js\" --sourcemap --platform=node --outdir=dist",
|
|
28
31
|
"cli": "node dist/cli.js",
|
|
29
|
-
"lint": "eslint src",
|
|
30
|
-
"lint:dev": "eslint --fix src",
|
|
31
|
-
"test:base": "
|
|
32
|
-
"test": "npm run lint && npm run test:base
|
|
33
|
-
"test
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"watch:test": "cross-env DEBUG=1 NODE_ENV=test nodemon -w src -w test-fixtures --exec 'npm run test:dev'"
|
|
32
|
+
"lint": "eslint ./src",
|
|
33
|
+
"lint:dev": "eslint --fix ./src",
|
|
34
|
+
"test:base": "node --test --test-force-exit --experimental-test-coverage --test-reporter=spec './src/*.test.js' './src/**/*.test.js'",
|
|
35
|
+
"test": "npm run lint && npm run test:base",
|
|
36
|
+
"watch:test": "cross-env DEBUG=@natlibfi/* NODE_ENV=test node --watch --test --experimental-test-coverage --test-reporter=spec './src/*.test.js' './src/**/*.test.js'",
|
|
37
|
+
"dev": "npm run watch:test",
|
|
38
|
+
"dev:debug": "cross-env LOG_LEVEL=debug DEBUG=@natlibfi/* NODE_ENV=test npm run watch:test"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@natlibfi/marc-record": "^
|
|
40
|
-
"@natlibfi/marc-record-serializers": "^
|
|
41
|
-
"@natlibfi/melinda-commons": "^
|
|
42
|
-
"@natlibfi/sru-client": "^
|
|
43
|
-
"debug": "^4.
|
|
44
|
-
"isbn3": "^1.
|
|
45
|
-
"moment": "^2.
|
|
46
|
-
"natural": "^
|
|
47
|
-
"uuid": "^
|
|
48
|
-
"winston": "^3.
|
|
41
|
+
"@natlibfi/marc-record": "^10.0.0-alpha.1",
|
|
42
|
+
"@natlibfi/marc-record-serializers": "^11.0.0-alpha.2",
|
|
43
|
+
"@natlibfi/melinda-commons": "^14.0.0-alpha.3",
|
|
44
|
+
"@natlibfi/sru-client": "^7.0.0-alpha.1",
|
|
45
|
+
"debug": "^4.4.1",
|
|
46
|
+
"isbn3": "^1.2.14",
|
|
47
|
+
"moment": "^2.30.1",
|
|
48
|
+
"natural": "^8.0.1",
|
|
49
|
+
"uuid": "^11.1.0",
|
|
50
|
+
"winston": "^3.17.0",
|
|
51
|
+
"yargs": "^18.0.0"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"@babel/preset-env": "^7.23.3",
|
|
55
|
-
"@babel/register": "^7.22.15",
|
|
56
|
-
"@natlibfi/eslint-config-melinda-backend": "^3.0.3",
|
|
57
|
-
"@natlibfi/fixugen": "^2.0.2",
|
|
58
|
-
"@natlibfi/fixugen-http-client": "^3.0.1",
|
|
59
|
-
"@natlibfi/fixura": "^3.0.2",
|
|
60
|
-
"babel-plugin-istanbul": "^6.1.1",
|
|
61
|
-
"babel-plugin-rewire": "^1.2.0",
|
|
62
|
-
"chai": "^4.3.10",
|
|
63
|
-
"chai-as-promised": "^7.1.1",
|
|
54
|
+
"@natlibfi/fixugen": "^3.0.0-alpha.6",
|
|
55
|
+
"@natlibfi/fixugen-http-client": "^4.0.0-alpha.1",
|
|
56
|
+
"@natlibfi/fixura": "^4.0.0-alpha.13",
|
|
64
57
|
"cross-env": "^7.0.3",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"nodemon": "^3.0.1",
|
|
68
|
-
"nyc": "^15.1.0"
|
|
69
|
-
},
|
|
70
|
-
"eslintConfig": {
|
|
71
|
-
"extends": "@natlibfi/melinda-backend"
|
|
72
|
-
},
|
|
73
|
-
"babel": {
|
|
74
|
-
"presets": [
|
|
75
|
-
[
|
|
76
|
-
"@babel/preset-env",
|
|
77
|
-
{
|
|
78
|
-
"targets": "maintained node versions"
|
|
79
|
-
}
|
|
80
|
-
]
|
|
81
|
-
],
|
|
82
|
-
"env": {
|
|
83
|
-
"test": {
|
|
84
|
-
"plugins": [
|
|
85
|
-
"istanbul",
|
|
86
|
-
"rewire"
|
|
87
|
-
]
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
"nyc": {
|
|
92
|
-
"exclude": [
|
|
93
|
-
"*/*.spec.js",
|
|
94
|
-
"*/**/*.spec.js"
|
|
95
|
-
],
|
|
96
|
-
"reporter": [
|
|
97
|
-
"lcov",
|
|
98
|
-
"html"
|
|
99
|
-
],
|
|
100
|
-
"require": [
|
|
101
|
-
"@babel/register"
|
|
102
|
-
],
|
|
103
|
-
"sourceMap": false,
|
|
104
|
-
"instrument": false,
|
|
105
|
-
"lines": 80,
|
|
106
|
-
"statements": 80,
|
|
107
|
-
"functions": 80,
|
|
108
|
-
"branches": 80
|
|
58
|
+
"esbuild": "^0.25.9",
|
|
59
|
+
"eslint": "^9.35.0"
|
|
109
60
|
}
|
|
110
61
|
}
|
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @licstart The following is the entire license notice for the JavaScript code in this file.
|
|
4
|
-
*
|
|
5
|
-
* Melinda record matching modules for Javascript
|
|
6
|
-
*
|
|
7
|
-
* Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
|
|
8
|
-
*
|
|
9
|
-
* This file is part of melinda-record-matching-js
|
|
10
|
-
*
|
|
11
|
-
* melinda-record-matching-js program is free software: you can redistribute it and/or modify
|
|
12
|
-
* it under the terms of the GNU Lesser General Public License as
|
|
13
|
-
* published by the Free Software Foundation, either version 3 of the
|
|
14
|
-
* License, or (at your option) any later version.
|
|
15
|
-
*
|
|
16
|
-
* melinda-record-matching-js is distributed in the hope that it will be useful,
|
|
17
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
-
* GNU Lesser General Public License for more details.
|
|
20
|
-
*
|
|
21
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
22
|
-
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
23
|
-
*
|
|
24
|
-
* @licend The above is the entire license notice
|
|
25
|
-
* for the JavaScript code in this file.
|
|
26
|
-
*
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
1
|
import createDebugLogger from 'debug';
|
|
30
2
|
|
|
31
3
|
export function toQueries(identifiers, queryString) {
|
|
@@ -33,6 +5,7 @@ export function toQueries(identifiers, queryString) {
|
|
|
33
5
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:toQueries');
|
|
34
6
|
const debugData = debug.extend('data');
|
|
35
7
|
|
|
8
|
+
// We quote the identifier, if it contains a slash! (Slash in non-quoted SRU-search breaks search.)
|
|
36
9
|
const quotedIdentifiers = identifiers.map(identifier => identifier.match(/\//u) ? `"${identifier}"` : `${identifier}`);
|
|
37
10
|
|
|
38
11
|
// Aleph supports only two queries with or -operator (This is not actually true)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import createDebugLogger from 'debug';
|
|
2
|
+
import createClient, {SruSearchError} from '@natlibfi/sru-client';
|
|
3
|
+
|
|
4
|
+
export class CandidateSearchError extends Error { }
|
|
5
|
+
|
|
6
|
+
export default async function ({url, queryList, queryListType, maxCandidates = 50}) {
|
|
7
|
+
|
|
8
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:choose-queries');
|
|
9
|
+
const debugData = debug.extend('data');
|
|
10
|
+
const debugDev = debug.extend('dev');
|
|
11
|
+
|
|
12
|
+
debugData(`Url: ${url}`);
|
|
13
|
+
debugData(`QueryList: ${queryList}`);
|
|
14
|
+
debugData(`queryListType: ${queryListType}`);
|
|
15
|
+
|
|
16
|
+
const client = createClient({
|
|
17
|
+
url,
|
|
18
|
+
maxRecordsPerRequest: 0,
|
|
19
|
+
version: '2.0',
|
|
20
|
+
retrieveAll: false
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
debugDev(`QueryList (type: ${queryListType}) ${JSON.stringify(queryList)}`);
|
|
24
|
+
try {
|
|
25
|
+
const {queriesWithTotals} = await getQueryTotals({queryList, queryOffset: 0, queriesWithTotals: []});
|
|
26
|
+
debugDev(`QueryResult: ${JSON.stringify(queriesWithTotals)}`);
|
|
27
|
+
const filteredQueryResult = filterQueryResult({queriesWithTotals, maxCandidates});
|
|
28
|
+
debugDev(`filteredQueryResult: ${JSON.stringify(filteredQueryResult)}`);
|
|
29
|
+
return filteredQueryResult;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new CandidateSearchError(err);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function getQueryTotals({queryList, queryOffset = 0, queriesWithTotals = []}) {
|
|
35
|
+
|
|
36
|
+
const query = queryList[queryOffset];
|
|
37
|
+
debug(`Running query ${JSON.stringify(query)} (${queryOffset}) for total`);
|
|
38
|
+
|
|
39
|
+
if (query) {
|
|
40
|
+
const {total} = await retrieveTotal();
|
|
41
|
+
|
|
42
|
+
const newQueriesWithTotals = [...queriesWithTotals, {query, total}];
|
|
43
|
+
debug(`Query ${queryOffset} ${query} done.`);
|
|
44
|
+
debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
|
|
45
|
+
return getQueryTotals({queryList, queryOffset: queryOffset + 1, queriesWithTotals: newQueriesWithTotals});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);
|
|
49
|
+
return {queriesWithTotals};
|
|
50
|
+
|
|
51
|
+
function retrieveTotal() {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
let totalRecords = 0;
|
|
54
|
+
|
|
55
|
+
debug(`Searching total amount of candidates for query: ${query}`);
|
|
56
|
+
|
|
57
|
+
client.searchRetrieve(query)
|
|
58
|
+
.on('error', err => {
|
|
59
|
+
if (err instanceof SruSearchError) {
|
|
60
|
+
debug(`SRU SruSearchError for query: ${query}: ${err}`);
|
|
61
|
+
reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));
|
|
62
|
+
}
|
|
63
|
+
debug(`SRU error for query: ${query}: ${err}`);
|
|
64
|
+
reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));
|
|
65
|
+
})
|
|
66
|
+
.on('total', total => {
|
|
67
|
+
debug(`Got total: ${total}`);
|
|
68
|
+
totalRecords += total;
|
|
69
|
+
})
|
|
70
|
+
.on('end', () => {
|
|
71
|
+
try {
|
|
72
|
+
resolve({total: totalRecords});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
debug(`Error caught on END`);
|
|
75
|
+
reject(err);
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.on('record', () => {
|
|
79
|
+
debugDev(`RECORD: We should no get records here`);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function filterQueryResult({queriesWithTotals, maxCandidates}) {
|
|
85
|
+
debug(`Filtering queries (${queriesWithTotals.length}), maxCandidates: ${maxCandidates}`);
|
|
86
|
+
debugData(`${JSON.stringify(queriesWithTotals)}`);
|
|
87
|
+
// Drop queries where total result is 0 or greater than given maxCandidates
|
|
88
|
+
const filteredQueryResult = queriesWithTotals.filter((queryWithTotal) => queryWithTotal.total !== 0 && queryWithTotal.total < maxCandidates);
|
|
89
|
+
debugData(`${JSON.stringify(filteredQueryResult)}`);
|
|
90
|
+
return filteredQueryResult;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
@@ -1,45 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @licstart The following is the entire license notice for the JavaScript code in this file.
|
|
4
|
-
*
|
|
5
|
-
* Melinda record matching modules for Javascript
|
|
6
|
-
*
|
|
7
|
-
* Copyright (C) 2020-2022 University Of Helsinki (The National Library Of Finland)
|
|
8
|
-
*
|
|
9
|
-
* This file is part of melinda-record-matching-js
|
|
10
|
-
*
|
|
11
|
-
* melinda-record-matching-js program is free software: you can redistribute it and/or modify
|
|
12
|
-
* it under the terms of the GNU Lesser General Public License as
|
|
13
|
-
* published by the Free Software Foundation, either version 3 of the
|
|
14
|
-
* License, or (at your option) any later version.
|
|
15
|
-
*
|
|
16
|
-
* melinda-record-matching-js is distributed in the hope that it will be useful,
|
|
17
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
-
* GNU Lesser General Public License for more details.
|
|
20
|
-
*
|
|
21
|
-
* You should have received a copy of the GNU Affero General Public License
|
|
22
|
-
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
23
|
-
*
|
|
24
|
-
* @licend The above is the entire license notice
|
|
25
|
-
* for the JavaScript code in this file.
|
|
26
|
-
*
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
1
|
import createDebugLogger from 'debug';
|
|
30
2
|
import createClient, {SruSearchError} from '@natlibfi/sru-client';
|
|
31
3
|
import {MarcRecord} from '@natlibfi/marc-record';
|
|
32
|
-
import {MARCXML} from '@natlibfi/marc-record-serializers';
|
|
33
|
-
import generateQueryList from './query-list';
|
|
34
|
-
import {Error as MatchingError} from '@natlibfi/melinda-commons';
|
|
35
4
|
|
|
36
|
-
|
|
5
|
+
import generateQueryList from './query-list/index.js';
|
|
6
|
+
import chooseQueries from './choose-queries.js';
|
|
7
|
+
|
|
8
|
+
export {searchTypes} from './query-list/index.js';
|
|
37
9
|
|
|
38
|
-
export class CandidateSearchError extends Error {}
|
|
10
|
+
export class CandidateSearchError extends Error { }
|
|
39
11
|
|
|
40
12
|
// serverMaxResults : maximum size of total search result available from the server, defaults to Aleph's 20000
|
|
41
13
|
|
|
42
|
-
export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {
|
|
14
|
+
export default async ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest = 50, serverMaxResult = 20000}) => {
|
|
43
15
|
MarcRecord.setValidationOptions({subfieldValues: false});
|
|
44
16
|
|
|
45
17
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search');
|
|
@@ -54,25 +26,43 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
|
|
|
54
26
|
// Do not retrieve more candidates than defined in maxCandidates
|
|
55
27
|
const adjustedMaxRecordsPerRequest = maxRecordsPerRequest >= maxCandidates ? maxCandidates : maxRecordsPerRequest;
|
|
56
28
|
|
|
57
|
-
const inputRecordId = getRecordId(record);
|
|
58
|
-
const queryList = generateQueryList(record, searchSpec);
|
|
59
29
|
const client = createClient({
|
|
60
30
|
url,
|
|
61
31
|
maxRecordsPerRequest: adjustedMaxRecordsPerRequest,
|
|
62
32
|
version: '2.0',
|
|
63
|
-
retrieveAll: false
|
|
33
|
+
retrieveAll: false,
|
|
34
|
+
metadataFormat: 'marcJson'
|
|
64
35
|
});
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
37
|
+
const inputRecordId = getRecordId(record);
|
|
38
|
+
const queryListResult = await generateQueryList(record, searchSpec, client);
|
|
39
|
+
const queryList = queryListResult[0]?.queryList ? queryListResult[0].queryList : queryListResult;
|
|
40
|
+
const queryListType = queryListResult[0]?.queryListType ? queryListResult[0].queryListType : undefined;
|
|
69
41
|
|
|
70
42
|
// if generateQueryList errored we should throw 422
|
|
71
|
-
|
|
72
43
|
if (queryList.length === 0) {
|
|
44
|
+
debug(`Empty list`);
|
|
73
45
|
throw new CandidateSearchError(`Generated query list contains no queries`);
|
|
74
46
|
}
|
|
47
|
+
if (queryListType && queryListType !== 'alternates') {
|
|
48
|
+
debug(`Unknown queryListType`);
|
|
49
|
+
throw new CandidateSearchError(`Generated query list has invalid type`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
debug(`Searching matches for ${inputRecordId}`);
|
|
53
|
+
const chosenQueryList = await filterQueryList({queryList, queryListType});
|
|
54
|
+
debug(`Chosen queries: ${JSON.stringify(chosenQueryList)}`);
|
|
55
|
+
|
|
56
|
+
async function filterQueryList({queryList, queryListType, maxCandidates}) {
|
|
57
|
+
debug(`Generated queryList (type: ${queryListType}) ${JSON.stringify(queryList)}`);
|
|
75
58
|
|
|
59
|
+
if (queryListType === 'alternates' && queryList.length > 1) {
|
|
60
|
+
const queryListResult = await chooseQueries({url, queryList, queryListType, maxCandidates});
|
|
61
|
+
debug(`queryListResult: ${JSON.stringify(queryListResult)}`);
|
|
62
|
+
return queryListResult.map(elem => elem.query);
|
|
63
|
+
}
|
|
64
|
+
return queryList;
|
|
65
|
+
}
|
|
76
66
|
// state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)
|
|
77
67
|
// state.query : current query (undefined if there was no queries left)
|
|
78
68
|
// state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)
|
|
@@ -81,98 +71,38 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
|
|
|
81
71
|
// state.queryCounter : sequence for current query
|
|
82
72
|
// state.maxedQueries : queries that resulted in more than serverMaxResults hits
|
|
83
73
|
|
|
74
|
+
return {search};
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
// eslint-disable-next-line max-statements
|
|
77
|
+
async function search({queryOffset = 0, resultSetOffset = 1, totalRecords = 0, searchCounter = 0, queryCandidateCounter = 0, queryCounter = 0, maxedQueries = []}) {
|
|
78
|
+
const query = chosenQueryList[queryOffset];
|
|
79
|
+
debug(`Running query ${JSON.stringify(query)} (${queryOffset})`);
|
|
87
80
|
|
|
88
81
|
if (query) {
|
|
89
|
-
const {records,
|
|
82
|
+
const {records, nextOffset, total} = await retrieveRecords(client, query, resultSetOffset);
|
|
90
83
|
|
|
91
84
|
// If resultSetOffset === 1 this is the first search for the current query
|
|
92
85
|
debugData(`ResultSetOffset: ${resultSetOffset}`);
|
|
93
86
|
const newTotalRecords = resultSetOffset === 1 ? total : totalRecords;
|
|
94
87
|
const newQueryCounter = resultSetOffset === 1 ? queryCounter + 1 : queryCounter;
|
|
95
88
|
const newSearchCounter = resultSetOffset === 1 ? 1 : searchCounter + 1;
|
|
96
|
-
const newQueryCandidateCounter = resultSetOffset === 1 ? records.length
|
|
89
|
+
const newQueryCandidateCounter = resultSetOffset === 1 ? records.length : queryCandidateCounter + records.length;
|
|
97
90
|
|
|
98
91
|
const maxedQuery = resultSetOffset === 1 ? checkMaxedQuery(query, total, serverMaxResult) : undefined;
|
|
99
92
|
const newMaxedQueries = maxedQuery ? maxedQueries.concat(maxedQuery) : maxedQueries;
|
|
100
93
|
|
|
101
94
|
if (typeof nextOffset === 'number') {
|
|
102
95
|
debug(`Next search will be for query ${queryOffset} ${query}, starting from record ${nextOffset}`);
|
|
103
|
-
return {records,
|
|
96
|
+
return {records, queryOffset, resultSetOffset: nextOffset, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};
|
|
104
97
|
}
|
|
105
98
|
debug(`Query ${queryOffset} ${query} done.`);
|
|
106
99
|
debug(`There are (${queryList.length - (queryOffset + 1)} queries left)`);
|
|
107
|
-
return {records,
|
|
100
|
+
return {records, queryOffset: queryOffset + 1, queriesLeft: queryList.length - (queryOffset + 1), totalRecords: newTotalRecords, query, searchCounter: newSearchCounter, queryCandidateCounter: newQueryCandidateCounter, queryCounter: newQueryCounter, maxedQueries: newMaxedQueries};
|
|
108
101
|
}
|
|
109
102
|
|
|
110
103
|
debug(`All ${queryList.length} queries done, there's no query for ${queryOffset}`);
|
|
111
|
-
return {records: [],
|
|
112
|
-
|
|
113
|
-
function retrieveRecords() {
|
|
114
|
-
return new Promise((resolve, reject) => {
|
|
115
|
-
const promises = [];
|
|
116
|
-
// eslint-disable-next-line functional/no-let
|
|
117
|
-
let totalRecords = 0;
|
|
118
|
-
|
|
119
|
-
debug(`Searching for candidates with query: ${query} (Offset ${resultSetOffset})`);
|
|
120
|
-
|
|
121
|
-
client.searchRetrieve(query, {startRecord: resultSetOffset})
|
|
122
|
-
.on('error', err => {
|
|
123
|
-
// eslint-disable-next-line functional/no-conditional-statements
|
|
124
|
-
if (err instanceof SruSearchError) {
|
|
125
|
-
debug(`SRU SruSearchError for query: ${query}: ${err}`);
|
|
126
|
-
reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));
|
|
127
|
-
}
|
|
128
|
-
debug(`SRU error for query: ${query}: ${err}`);
|
|
129
|
-
reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));
|
|
130
|
-
})
|
|
131
|
-
.on('total', total => {
|
|
132
|
-
debug(`Got total: ${total}`);
|
|
133
|
-
totalRecords += total;
|
|
134
|
-
})
|
|
135
|
-
.on('end', async nextOffset => {
|
|
136
|
-
try {
|
|
137
|
-
const recordPromises = await Promise.allSettled(promises);
|
|
138
|
-
debugData(`All recordPromises: ${JSON.stringify(recordPromises)}`);
|
|
139
|
-
const filtered = recordPromises.filter(r => r.status === 'fulfilled').map(r => r.value);
|
|
140
|
-
const failures = recordPromises.filter(r => r.status === 'rejected').map(r => ({status: r.reason.status, payload: r.reason.payload}));
|
|
141
|
-
|
|
142
|
-
debug(`Found ${recordPromises.length} records`);
|
|
143
|
-
debug(`Found ${filtered.length} convertable candidates`);
|
|
144
|
-
debug(`Found ${failures.length} NON-convertable candidates`);
|
|
145
|
-
debugData(`Converted: ${JSON.stringify(filtered)}.`);
|
|
146
|
-
debugData(`Not converted: ${JSON.stringify(failures)}.`);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
resolve({nextOffset, records: filtered, failures, total: totalRecords});
|
|
150
|
-
} catch (err) {
|
|
151
|
-
debug(`Error caught on END`);
|
|
152
|
-
reject(err);
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
.on('record', recordXML => {
|
|
156
|
-
promises.push(handleRecord()); // eslint-disable-line functional/immutable-data
|
|
157
|
-
|
|
158
|
-
async function handleRecord() {
|
|
159
|
-
try {
|
|
160
|
-
const recordMarc = await MARCXML.from(recordXML, {subfieldValues: false});
|
|
161
|
-
const recordId = getRecordId(recordMarc);
|
|
162
|
-
|
|
163
|
-
return {record: recordMarc, id: recordId};
|
|
164
|
-
} catch (err) {
|
|
165
|
-
// What should this do?
|
|
166
|
-
const idFromXML = getRecordIdFromXML(recordXML);
|
|
167
|
-
debugData(`Failed converting record: ${err.message}, id: ${idFromXML}, data: ${recordXML}`);
|
|
168
|
-
//return {message: `Failed converting record: ${err.message}`, id: idFromXML, data: recordXML};
|
|
169
|
-
throw new MatchingError(422, {message: `Failed converting record: ${err.message}`, id: idFromXML || '000000000', data: recordXML});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
};
|
|
104
|
+
return {records: [], queriesLeft: 0, queryCounter, maxedQueries};
|
|
105
|
+
}
|
|
176
106
|
|
|
177
107
|
function checkMaxedQuery(query, total, serverMaxResult) {
|
|
178
108
|
if (total >= serverMaxResult) {
|
|
@@ -181,16 +111,52 @@ export default ({record, searchSpec, url, maxCandidates, maxRecordsPerRequest =
|
|
|
181
111
|
}
|
|
182
112
|
}
|
|
183
113
|
|
|
184
|
-
|
|
185
114
|
function getRecordId(record) {
|
|
186
115
|
const [field] = record.get(/^001$/u);
|
|
187
116
|
return field ? field.value : '';
|
|
188
117
|
}
|
|
118
|
+
};
|
|
189
119
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
120
|
+
export function retrieveRecords(client, query, resultSetOffset) {
|
|
121
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:retrieveRecords');
|
|
122
|
+
// eslint-disable-next-line no-unused-vars
|
|
123
|
+
const debugData = debug.extend('data');
|
|
195
124
|
|
|
196
|
-
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const records = [];
|
|
127
|
+
let totalRecords = 0;
|
|
128
|
+
|
|
129
|
+
debug(`Searching for candidates with query: ${query} (Offset ${resultSetOffset})`);
|
|
130
|
+
|
|
131
|
+
client.searchRetrieve(query, {startRecord: resultSetOffset})
|
|
132
|
+
.on('error', err => {
|
|
133
|
+
if (err instanceof SruSearchError) {
|
|
134
|
+
debug(`SRU SruSearchError for query: ${query}: ${err}`);
|
|
135
|
+
reject(new CandidateSearchError(`SRU SruSearchError for query: ${query}: ${err}`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
debug(`SRU error for query: ${query}: ${err}`);
|
|
139
|
+
reject(new CandidateSearchError(`SRU error for query: ${query}: ${err}`));
|
|
140
|
+
})
|
|
141
|
+
.on('total', total => {
|
|
142
|
+
debug(`Got total: ${total}`);
|
|
143
|
+
totalRecords += total;
|
|
144
|
+
})
|
|
145
|
+
.on('end', nextOffset => {
|
|
146
|
+
try {
|
|
147
|
+
debug(`Found ${records.length} records`);
|
|
148
|
+
|
|
149
|
+
resolve({nextOffset, records, total: totalRecords});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
debug(`Error caught on END`);
|
|
152
|
+
reject(err);
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.on('record', record => {
|
|
156
|
+
const [field] = record.get(/^001$/u);
|
|
157
|
+
debug(field);
|
|
158
|
+
const id = field.value ? field.value : '';
|
|
159
|
+
records.push({record, id});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|