Skip to content

Commit 476ec8b

Browse files
authored
Merge pull request #371 from nulib/deploy/staging
Deploy v2.11.0 to production
2 parents 4ebc8bd + cbf8222 commit 476ec8b

60 files changed

Lines changed: 2476 additions & 189 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ View and edit information about a specific Work in the Index.
5151
3. View JSON response at `https://USER_PREFIX.dev.rdc.library.northwestern.edu:3002/works/[WORK_ID]`
5252
4. View IIIF Manifest JSON response at `https://USER_PREFIX.dev.rdc.library.northwestern.edu:3002/works/[WORK_ID]?as=iiif`
5353

54+
### IIIF content search
55+
56+
IIIF Presentation responses expose [IIIF Content Search 2.0](https://iiif.io/api/search/2.0/) services for transcription annotations:
57+
58+
- Work manifests include a `SearchService2` entry for `https://USER_PREFIX.dev.rdc.library.northwestern.edu:3002/works/[WORK_ID]/search?as=iiif`
59+
- File set canvases include a `SearchService2` entry for `https://USER_PREFIX.dev.rdc.library.northwestern.edu:3002/file-sets/[FILE_SET_ID]/search?as=iiif`
60+
61+
To search transcription text, include a non-empty `q` parameter:
62+
63+
```shell
64+
curl "https://USER_PREFIX.dev.rdc.library.northwestern.edu:3002/works/[WORK_ID]/search?as=iiif&q=[QUERY]"
65+
curl "https://USER_PREFIX.dev.rdc.library.northwestern.edu:3002/file-sets/[FILE_SET_ID]/search?as=iiif&q=[QUERY]"
66+
```
67+
68+
Both endpoints return a IIIF `AnnotationPage` whose `items` target the matching work canvas or file set canvas. Requests without `as=iiif` or a non-empty `q` return `400`.
69+
5470
For help debugging/inspecting, JavaScript `console` messages are written to: `dc-api-v2/dc-api.log`
5571

5672
### DC

api/dependencies/package-lock.json

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

api/dependencies/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dc-api-dependencies",
3-
"version": "2.10.10",
3+
"version": "2.11.0",
44
"description": "NUL Digital Collections API Dependencies",
55
"repository": "https://github.com/nulib/dc-api-v2",
66
"author": "nulib",

api/package-lock.json

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

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dc-api-build",
3-
"version": "2.10.10",
3+
"version": "2.11.0",
44
"description": "NUL Digital Collections API Build Environment",
55
"repository": "https://github.com/nulib/dc-api-v2",
66
"author": "nulib",

api/src/api/opensearch.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function getWorkFileSets(workId, opts = {}) {
2727
const {
2828
allowPrivate = false,
2929
allowUnpublished = false,
30+
annotationsQuery = null,
3031
role = null,
3132
source = null,
3233
sortBy = null,
@@ -52,6 +53,11 @@ async function getWorkFileSets(workId, opts = {}) {
5253
if (role) {
5354
mustClauses.push({ term: { role: role } });
5455
}
56+
if (annotationsQuery) {
57+
mustClauses.push({
58+
match_phrase: { "annotations.content": annotationsQuery },
59+
});
60+
}
5561

5662
const searchBody = {
5763
size: 10000,

api/src/api/pagination.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ const {
44
} = require("lz-string");
55
const { defaultSearchSize } = require("../environment");
66

7-
const encodeFields = ["query", "size", "sort", "fields", "_source"];
7+
const encodeFields = [
8+
"query",
9+
"size",
10+
"sort",
11+
"fields",
12+
"collapse",
13+
"aggs",
14+
"_source",
15+
];
816

917
async function decodeSearchToken(token) {
1018
return JSON.parse(await decompress(token));
@@ -17,6 +25,7 @@ async function encodeSearchToken(models, body, format, options) {
1725
token.body[field] = body[field];
1826
}
1927
}
28+
if (token.body.aggs?._pagination) delete token.body.aggs._pagination;
2029
return await compress(JSON.stringify(token));
2130
}
2231

@@ -55,6 +64,13 @@ class Paginator {
5564
this.options = options;
5665
}
5766

67+
async pageResponseInfo(responseBody, opts = {}) {
68+
return this.pageInfo(responseBody.hits.total.value, {
69+
aggregatedCount: responseBody.hits.collapsed?.value,
70+
...opts,
71+
});
72+
}
73+
5874
async pageInfo(count, opts = {}) {
5975
let url = new URL(this.route, this.baseUrl);
6076
let searchToken;
@@ -74,15 +90,18 @@ class Paginator {
7490
}
7591

7692
const queryStringParameters =
77-
this.options?.parameterOverrides || this.options?.queryStringParameters;
93+
this.options?.parameterOverrides ||
94+
this.options?.queryStringParameters ||
95+
{};
7896
if (typeof queryStringParameters === "object") {
7997
for (const param in queryStringParameters) {
8098
url.searchParams.set(param, queryStringParameters[param]);
8199
}
82100
}
83101

84-
const prev = prevPage(this.body, count);
85-
const next = nextPage(this.body, count);
102+
const aggregatedCount = opts?.aggregatedCount || count;
103+
const prev = prevPage(this.body, aggregatedCount);
104+
const next = nextPage(this.body, aggregatedCount);
86105
url.searchParams.delete("from");
87106

88107
let result = {
@@ -91,9 +110,15 @@ class Paginator {
91110
limit: size(this.body),
92111
offset: from(this.body),
93112
total_hits: count,
94-
total_pages: maxPage(this.body, count),
113+
total_pages: maxPage(this.body, aggregatedCount),
95114
format: this.format,
96115
};
116+
if (this.body.collapse) {
117+
result.collapsed_by = {
118+
field: this.body.collapse.field,
119+
total_hits: aggregatedCount,
120+
};
121+
}
97122
if (opts.includeOptions) {
98123
result.options = this.options;
99124
}

api/src/api/request/pipeline.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,20 @@ module.exports = class RequestPipeline {
104104
return this;
105105
}
106106

107+
addCardinality() {
108+
if (this.searchContext.collapse) {
109+
this.searchContext.aggs ||= {};
110+
this.searchContext.aggs.__pagination = {
111+
cardinality: {
112+
field: this.searchContext.collapse.field,
113+
},
114+
};
115+
}
116+
return this;
117+
}
118+
107119
toJson() {
108-
this.addNeuralModelId();
120+
this.addNeuralModelId().addCardinality();
109121
return JSON.stringify(sortJson(this.searchContext));
110122
}
111123
};
Lines changed: 19 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,28 @@
11
const { dcApiEndpoint } = require("../../../environment");
2-
const { getWorkFileSets } = require("../../opensearch");
2+
const {
3+
buildAnnotationTarget,
4+
buildSearchAnnotationBody,
5+
} = require("./search-helpers");
36

4-
async function transform(response, options = {}) {
5-
const body = JSON.parse(response.body);
6-
const fileSet = body._source;
7-
const annotations = fileSet?.annotations ?? [];
8-
9-
const workId = fileSet.work_id;
10-
const fileSetId = body._id;
11-
const fileSetIndex = await getFileSetIndex(workId, fileSetId, options);
12-
13-
const canvasId = `${dcApiEndpoint()}/works/${workId}?as=iiif/canvas/${fileSetIndex}`;
14-
const annotationPageId = `${dcApiEndpoint()}/file-sets/${
15-
fileSet.id
16-
}/annotations?as=iiif`;
17-
18-
// Build annotation items - filter for transcriptions only
19-
// We currently will only have one annotation and it's a transcription
20-
const items = annotations
21-
.filter((annotation) => annotation.type === "transcription")
22-
.map((annotation, idx) => {
23-
const annotationId = `${annotationPageId}/a${idx}`;
24-
return {
25-
id: annotationId,
26-
type: "Annotation",
27-
motivation: "commenting",
28-
body: {
29-
type: "TextualBody",
30-
value: annotation.content,
31-
format: "text/plain",
32-
language: annotation.language || "en",
33-
},
34-
target: canvasId,
35-
};
36-
});
37-
38-
const annotationPage = {
39-
"@context": "http://iiif.io/api/presentation/3/context.json",
40-
id: annotationPageId,
41-
type: "AnnotationPage",
42-
items: items,
43-
};
7+
function transform(annotation, fileSet) {
8+
const canvasId = `${dcApiEndpoint()}/file-sets/${fileSet.id}?as=iiif`;
9+
const annotationId = `${dcApiEndpoint()}/annotations/${
10+
annotation.id
11+
}?as=iiif`;
4412

4513
return {
4614
statusCode: 200,
47-
headers: {
48-
"content-type": "application/json",
49-
},
50-
body: JSON.stringify(annotationPage),
15+
headers: { "content-type": "application/json" },
16+
body: JSON.stringify({
17+
"@context": "http://iiif.io/api/presentation/3/context.json",
18+
id: annotationId,
19+
type: "Annotation",
20+
// We have hardcoded motivations here, but in the future we may want to make this more dynamic based on the annotation type
21+
motivation: ["contentState", "commenting"],
22+
body: buildSearchAnnotationBody(annotation),
23+
target: buildAnnotationTarget(canvasId, fileSet.work_id),
24+
}),
5125
};
5226
}
5327

54-
async function getFileSetIndex(workId, fileSetId, options) {
55-
const fileSetsResponse = await getWorkFileSets(workId, {
56-
allowPrivate: options.allowPrivate,
57-
allowUnpublished: options.allowUnpublished,
58-
role: "Access",
59-
sortBy: "rank",
60-
});
61-
62-
const fileSetBody = JSON.parse(fileSetsResponse.body);
63-
const hits = fileSetBody?.hits?.hits || [];
64-
65-
const index = hits.findIndex((hit) => hit._source.id === fileSetId);
66-
67-
return index;
68-
}
6928
module.exports = { transform };

0 commit comments

Comments
 (0)