From 748a08fe2ab8ca997a12eab65b35c58f601e900a Mon Sep 17 00:00:00 2001 From: Ian O'Connor Date: Tue, 6 Jan 2026 10:22:20 -0500 Subject: [PATCH 1/9] Support multi-word prefixes in "containing" search --- .../elastic-query-subjects-builder.js | 1 + lib/subjects.js | 2 +- test/elastic-query-subjects-builder.test.js | 14 +++++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/elasticsearch/elastic-query-subjects-builder.js b/lib/elasticsearch/elastic-query-subjects-builder.js index 78c685f7..2e8ed638 100644 --- a/lib/elasticsearch/elastic-query-subjects-builder.js +++ b/lib/elasticsearch/elastic-query-subjects-builder.js @@ -53,6 +53,7 @@ class ElasticQuerySubjectsBuilder { bool: { should: [ { match: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTerm' } } }, + { match_bool_prefix: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTermMatchBoolPrefix' } } }, { prefix: { preferredTerm: { value: this.request.querySansQuotes(), _name: 'preferredTermPrefix' } } }, { nested: { path: 'variants', query: { match: { 'variants.variant': { query: this.request.querySansQuotes(), operator: 'and' } } }, inner_hits: {} } } ] diff --git a/lib/subjects.js b/lib/subjects.js index 19e27817..2195b120 100644 --- a/lib/subjects.js +++ b/lib/subjects.js @@ -54,7 +54,7 @@ module.exports = function (app, _private = null) { per_page: params.per_page, totalResults: resp.hits?.total?.value, subjects: resp.hits?.hits?.map((hit) => { - if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix') { // if match is on preferredTerm, use that regardless of variant matches + if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix' || hit.matched_queries?.[0] === 'preferredTermMatchBoolPrefix') { // if match is on preferredTerm, use that regardless of variant matches return { '@type': 'preferredTerm', termLabel: hit._source.preferredTerm, diff --git a/test/elastic-query-subjects-builder.test.js b/test/elastic-query-subjects-builder.test.js index 889a5fb0..fcf53278 100644 --- a/test/elastic-query-subjects-builder.test.js +++ b/test/elastic-query-subjects-builder.test.js @@ -43,7 +43,7 @@ describe('ElasticQuerySubjectsBuilder', () => { const query = inst.query.toJson() - expect(query.bool.must[0].bool.should.length).to.equal(3) + expect(query.bool.must[0].bool.should.length).to.equal(4) expect(query.bool.must[0].bool.should[0]) expect(query.bool.must[0].bool.should[0]).to.deep.equal({ match: { @@ -54,14 +54,22 @@ describe('ElasticQuerySubjectsBuilder', () => { } } }) - expect(query.bool.must[0].bool.should[1]).to.deep.equal({ + match_bool_prefix: { + preferredTerm: { + _name: 'preferredTermMatchBoolPrefix', + query: 'toast', + operator: 'and' + } + } + }) + expect(query.bool.must[0].bool.should[2]).to.deep.equal({ prefix: { preferredTerm: { value: 'toast', _name: 'preferredTermPrefix' } } }) - expect(query.bool.must[0].bool.should[2]).to.deep.equal({ + expect(query.bool.must[0].bool.should[3]).to.deep.equal({ nested: { inner_hits: {}, path: 'variants', From 48787bb6f09c20b8d7098838fd622f7f388ccc9e Mon Sep 17 00:00:00 2001 From: Ian O'Connor Date: Tue, 6 Jan 2026 12:37:21 -0500 Subject: [PATCH 2/9] Ensure ordering works properly --- .../elastic-query-subjects-builder.js | 18 +++++- lib/subjects.js | 2 +- test/elastic-query-subjects-builder.test.js | 60 +++++++++++++++---- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/lib/elasticsearch/elastic-query-subjects-builder.js b/lib/elasticsearch/elastic-query-subjects-builder.js index 2e8ed638..cf8dd67e 100644 --- a/lib/elasticsearch/elastic-query-subjects-builder.js +++ b/lib/elasticsearch/elastic-query-subjects-builder.js @@ -49,11 +49,27 @@ class ElasticQuerySubjectsBuilder { * Require general "and" match on provided query */ requireContainingMatch () { + const words = this.request.querySansQuotes().split(/\s+/).filter(Boolean) + const multiPrefix = [] + + // build a bool query that supports prefixes for each word while still requiring the remaining words + words.forEach(word => { + const otherWords = words.filter(w => w !== word).join(' ') + + const mustClause = [] + mustClause.push({ prefix: { preferredTerm: { value: word } } }) + if (otherWords) { + mustClause.push({ match: { preferredTerm: { query: otherWords, operator: 'and' } } }) + } + + multiPrefix.push({ bool: { must: mustClause } }) + }) + this.query.addMust({ bool: { should: [ { match: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTerm' } } }, - { match_bool_prefix: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTermMatchBoolPrefix' } } }, + { bool: { should: multiPrefix, _name: 'preferredTermMultiPrefix' } }, { prefix: { preferredTerm: { value: this.request.querySansQuotes(), _name: 'preferredTermPrefix' } } }, { nested: { path: 'variants', query: { match: { 'variants.variant': { query: this.request.querySansQuotes(), operator: 'and' } } }, inner_hits: {} } } ] diff --git a/lib/subjects.js b/lib/subjects.js index 2195b120..45cd85c2 100644 --- a/lib/subjects.js +++ b/lib/subjects.js @@ -54,7 +54,7 @@ module.exports = function (app, _private = null) { per_page: params.per_page, totalResults: resp.hits?.total?.value, subjects: resp.hits?.hits?.map((hit) => { - if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix' || hit.matched_queries?.[0] === 'preferredTermMatchBoolPrefix') { // if match is on preferredTerm, use that regardless of variant matches + if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix' || hit.matched_queries?.[0] === 'preferredTermMultiPrefix') { // if match is on preferredTerm, use that regardless of variant matches return { '@type': 'preferredTerm', termLabel: hit._source.preferredTerm, diff --git a/test/elastic-query-subjects-builder.test.js b/test/elastic-query-subjects-builder.test.js index fcf53278..91eba7d3 100644 --- a/test/elastic-query-subjects-builder.test.js +++ b/test/elastic-query-subjects-builder.test.js @@ -38,7 +38,7 @@ describe('ElasticQuerySubjectsBuilder', () => { describe('search_scope="has"', () => { it('applies subject match clauses to query', () => { - const request = new ApiRequest({ q: 'toast', search_scope: 'has' }) + const request = new ApiRequest({ q: 'toast bread', search_scope: 'has' }) const inst = ElasticQuerySubjectsBuilder.forApiRequest(request) const query = inst.query.toJson() @@ -49,23 +49,63 @@ describe('ElasticQuerySubjectsBuilder', () => { match: { preferredTerm: { _name: 'preferredTerm', - query: 'toast', + query: 'toast bread', operator: 'and' } } }) expect(query.bool.must[0].bool.should[1]).to.deep.equal({ - match_bool_prefix: { - preferredTerm: { - _name: 'preferredTermMatchBoolPrefix', - query: 'toast', - operator: 'and' - } + bool: { + _name: 'preferredTermMultiPrefix', + should: [ + { + bool: { + must: [ + { + prefix: { + preferredTerm: { + value: 'toast' + } + } + }, + { + match: { + preferredTerm: { + query: 'bread', + operator: 'and' + } + } + } + ] + } + }, + { + bool: { + must: [ + { + prefix: { + preferredTerm: { + value: 'bread' + } + } + }, + { + match: { + preferredTerm: { + query: 'toast', + operator: 'and' + } + } + } + ] + } + } + ] } }) expect(query.bool.must[0].bool.should[2]).to.deep.equal({ prefix: { - preferredTerm: { value: 'toast', _name: 'preferredTermPrefix' } + preferredTerm: { value: 'toast bread', _name: 'preferredTermPrefix' } } }) @@ -76,7 +116,7 @@ describe('ElasticQuerySubjectsBuilder', () => { query: { match: { 'variants.variant': { - query: 'toast', + query: 'toast bread', operator: 'and' } } From 9727568f1a81515f2ec11b1de77953faf68d3737 Mon Sep 17 00:00:00 2001 From: Ian O'Connor Date: Tue, 6 Jan 2026 12:44:10 -0500 Subject: [PATCH 3/9] Remove duplicate prefix query clause --- lib/elasticsearch/elastic-query-subjects-builder.js | 3 +-- lib/subjects.js | 2 +- test/elastic-query-subjects-builder.test.js | 11 +++-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/elasticsearch/elastic-query-subjects-builder.js b/lib/elasticsearch/elastic-query-subjects-builder.js index cf8dd67e..e0ab32c2 100644 --- a/lib/elasticsearch/elastic-query-subjects-builder.js +++ b/lib/elasticsearch/elastic-query-subjects-builder.js @@ -69,8 +69,7 @@ class ElasticQuerySubjectsBuilder { bool: { should: [ { match: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTerm' } } }, - { bool: { should: multiPrefix, _name: 'preferredTermMultiPrefix' } }, - { prefix: { preferredTerm: { value: this.request.querySansQuotes(), _name: 'preferredTermPrefix' } } }, + { bool: { should: multiPrefix, _name: 'preferredTermPrefix' } }, { nested: { path: 'variants', query: { match: { 'variants.variant': { query: this.request.querySansQuotes(), operator: 'and' } } }, inner_hits: {} } } ] } diff --git a/lib/subjects.js b/lib/subjects.js index 45cd85c2..19e27817 100644 --- a/lib/subjects.js +++ b/lib/subjects.js @@ -54,7 +54,7 @@ module.exports = function (app, _private = null) { per_page: params.per_page, totalResults: resp.hits?.total?.value, subjects: resp.hits?.hits?.map((hit) => { - if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix' || hit.matched_queries?.[0] === 'preferredTermMultiPrefix') { // if match is on preferredTerm, use that regardless of variant matches + if (hit.matched_queries?.[0] === 'preferredTerm' || hit.matched_queries?.[0] === 'preferredTermPrefix') { // if match is on preferredTerm, use that regardless of variant matches return { '@type': 'preferredTerm', termLabel: hit._source.preferredTerm, diff --git a/test/elastic-query-subjects-builder.test.js b/test/elastic-query-subjects-builder.test.js index 91eba7d3..960de3f8 100644 --- a/test/elastic-query-subjects-builder.test.js +++ b/test/elastic-query-subjects-builder.test.js @@ -43,7 +43,7 @@ describe('ElasticQuerySubjectsBuilder', () => { const query = inst.query.toJson() - expect(query.bool.must[0].bool.should.length).to.equal(4) + expect(query.bool.must[0].bool.should.length).to.equal(3) expect(query.bool.must[0].bool.should[0]) expect(query.bool.must[0].bool.should[0]).to.deep.equal({ match: { @@ -56,7 +56,7 @@ describe('ElasticQuerySubjectsBuilder', () => { }) expect(query.bool.must[0].bool.should[1]).to.deep.equal({ bool: { - _name: 'preferredTermMultiPrefix', + _name: 'preferredTermPrefix', should: [ { bool: { @@ -103,13 +103,8 @@ describe('ElasticQuerySubjectsBuilder', () => { ] } }) - expect(query.bool.must[0].bool.should[2]).to.deep.equal({ - prefix: { - preferredTerm: { value: 'toast bread', _name: 'preferredTermPrefix' } - } - }) - expect(query.bool.must[0].bool.should[3]).to.deep.equal({ + expect(query.bool.must[0].bool.should[2]).to.deep.equal({ nested: { inner_hits: {}, path: 'variants', From 35eb47c44e73ed37ef5ef4c3ed360911fd389ef2 Mon Sep 17 00:00:00 2001 From: Ian O'Connor Date: Thu, 15 Jan 2026 15:10:37 -0500 Subject: [PATCH 4/9] Simplify stemming solution --- .../elastic-query-subjects-builder.js | 21 +++------ test/elastic-query-subjects-builder.test.js | 46 ++++--------------- 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/lib/elasticsearch/elastic-query-subjects-builder.js b/lib/elasticsearch/elastic-query-subjects-builder.js index e0ab32c2..0d9a15a5 100644 --- a/lib/elasticsearch/elastic-query-subjects-builder.js +++ b/lib/elasticsearch/elastic-query-subjects-builder.js @@ -50,26 +50,19 @@ class ElasticQuerySubjectsBuilder { */ requireContainingMatch () { const words = this.request.querySansQuotes().split(/\s+/).filter(Boolean) - const multiPrefix = [] - - // build a bool query that supports prefixes for each word while still requiring the remaining words - words.forEach(word => { - const otherWords = words.filter(w => w !== word).join(' ') - - const mustClause = [] - mustClause.push({ prefix: { preferredTerm: { value: word } } }) - if (otherWords) { - mustClause.push({ match: { preferredTerm: { query: otherWords, operator: 'and' } } }) + const multiPrefix = words.map(word => ({ + prefix: { + preferredTerm: { + value: word.toLowerCase() + } } - - multiPrefix.push({ bool: { must: mustClause } }) - }) + })) this.query.addMust({ bool: { should: [ { match: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTerm' } } }, - { bool: { should: multiPrefix, _name: 'preferredTermPrefix' } }, + { bool: { must: multiPrefix, _name: 'preferredTermPrefix' } }, { nested: { path: 'variants', query: { match: { 'variants.variant': { query: this.request.querySansQuotes(), operator: 'and' } } }, inner_hits: {} } } ] } diff --git a/test/elastic-query-subjects-builder.test.js b/test/elastic-query-subjects-builder.test.js index 960de3f8..36601e4d 100644 --- a/test/elastic-query-subjects-builder.test.js +++ b/test/elastic-query-subjects-builder.test.js @@ -57,47 +57,19 @@ describe('ElasticQuerySubjectsBuilder', () => { expect(query.bool.must[0].bool.should[1]).to.deep.equal({ bool: { _name: 'preferredTermPrefix', - should: [ + must: [ { - bool: { - must: [ - { - prefix: { - preferredTerm: { - value: 'toast' - } - } - }, - { - match: { - preferredTerm: { - query: 'bread', - operator: 'and' - } - } - } - ] + prefix: { + preferredTerm: { + value: 'toast' + } } }, { - bool: { - must: [ - { - prefix: { - preferredTerm: { - value: 'bread' - } - } - }, - { - match: { - preferredTerm: { - query: 'toast', - operator: 'and' - } - } - } - ] + prefix: { + preferredTerm: { + value: 'bread' + } } } ] From 567838ba22afdcb11424ef2ac3648fcc6fc49744 Mon Sep 17 00:00:00 2001 From: Ian O'Connor Date: Thu, 15 Jan 2026 16:54:20 -0500 Subject: [PATCH 5/9] Just use a new index with a edge ngram tokenizer --- config/qa.env | 2 +- .../elastic-query-subjects-builder.js | 10 -------- test/elastic-query-subjects-builder.test.js | 25 ++----------------- 3 files changed, 3 insertions(+), 34 deletions(-) diff --git a/config/qa.env b/config/qa.env index f7cfb2f4..a802f3f5 100644 --- a/config/qa.env +++ b/config/qa.env @@ -1,6 +1,6 @@ ENCRYPTED_ELASTICSEARCH_URI=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJYwgZMGCSqGSIb3DQEHBqCBhTCBggIBADB9BgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDMIkDoQ9C/cCDCAq1wIBEIBQ+L3OgUGeOW9rs1CWkhpBjwM4LbbVRFIWedqew4UXIeSNMJ8cO9SNe4YGCUIoKwCDYt7W7ip3VtDRRRMVvz6QJw+Eg8ugTMVs2pbNFGNvaAQ= RESOURCES_INDEX=resources-2025-07-07 -SUBJECTS_INDEX=browse-qa-2025-10-27 +SUBJECTS_INDEX=browse-qa-2026-01-15 ENCRYPTED_ELASTICSEARCH_API_KEY=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJ4wgZsGCSqGSIb3DQEHBqCBjTCBigIBADCBhAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAx+kryf2KUmGdBYD9sCARCAV3ygz3eXIdq8JX/wpG9JRWlTNMRcpNE1qT0zNlN4t+ZvXEoedLQa/3p1YjgHw06GIAdA9xtkMV4eH9a1K8uCvjP8XxxNKekcMj59TlResnu9QF3r7pGXuQ== ENCRYPTED_SCSB_URL=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAH8wfQYJKoZIhvcNAQcGoHAwbgIBADBpBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDBKllElmWYLxGOGopQIBEIA8JJyKde/8m8iCJGKR5D8HoTJhXHeyvw9eIDeuUNKiXLfJwoVz+PDAZSxkCQtM9O91zGhXbe3l6Bk1RlYJ diff --git a/lib/elasticsearch/elastic-query-subjects-builder.js b/lib/elasticsearch/elastic-query-subjects-builder.js index 0d9a15a5..1a3b8fe6 100644 --- a/lib/elasticsearch/elastic-query-subjects-builder.js +++ b/lib/elasticsearch/elastic-query-subjects-builder.js @@ -49,20 +49,10 @@ class ElasticQuerySubjectsBuilder { * Require general "and" match on provided query */ requireContainingMatch () { - const words = this.request.querySansQuotes().split(/\s+/).filter(Boolean) - const multiPrefix = words.map(word => ({ - prefix: { - preferredTerm: { - value: word.toLowerCase() - } - } - })) - this.query.addMust({ bool: { should: [ { match: { preferredTerm: { query: this.request.querySansQuotes(), operator: 'and', _name: 'preferredTerm' } } }, - { bool: { must: multiPrefix, _name: 'preferredTermPrefix' } }, { nested: { path: 'variants', query: { match: { 'variants.variant': { query: this.request.querySansQuotes(), operator: 'and' } } }, inner_hits: {} } } ] } diff --git a/test/elastic-query-subjects-builder.test.js b/test/elastic-query-subjects-builder.test.js index 36601e4d..a50f7539 100644 --- a/test/elastic-query-subjects-builder.test.js +++ b/test/elastic-query-subjects-builder.test.js @@ -43,7 +43,7 @@ describe('ElasticQuerySubjectsBuilder', () => { const query = inst.query.toJson() - expect(query.bool.must[0].bool.should.length).to.equal(3) + expect(query.bool.must[0].bool.should.length).to.equal(2) expect(query.bool.must[0].bool.should[0]) expect(query.bool.must[0].bool.should[0]).to.deep.equal({ match: { @@ -54,29 +54,8 @@ describe('ElasticQuerySubjectsBuilder', () => { } } }) - expect(query.bool.must[0].bool.should[1]).to.deep.equal({ - bool: { - _name: 'preferredTermPrefix', - must: [ - { - prefix: { - preferredTerm: { - value: 'toast' - } - } - }, - { - prefix: { - preferredTerm: { - value: 'bread' - } - } - } - ] - } - }) - expect(query.bool.must[0].bool.should[2]).to.deep.equal({ + expect(query.bool.must[0].bool.should[1]).to.deep.equal({ nested: { inner_hits: {}, path: 'variants', From 80b920e41878537e1f36f1d982059b5ae262855b Mon Sep 17 00:00:00 2001 From: Ian O'Connor Date: Thu, 15 Jan 2026 17:01:15 -0500 Subject: [PATCH 6/9] no-op change to force github state sync --- lib/subjects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/subjects.js b/lib/subjects.js index 19e27817..5a5e533b 100644 --- a/lib/subjects.js +++ b/lib/subjects.js @@ -65,7 +65,7 @@ module.exports = function (app, _private = null) { uri: hit._source.uri } } else { - // Match was on a variant- use that in the response. + // Match was on a variant- use that in the response const matchedVariantTerm = hit.inner_hits.variants.hits.hits[0]._source.variant return { From 13afccf1c5ef44af1bd0cb24bacaf005dea42c5b Mon Sep 17 00:00:00 2001 From: Paul Beaudoin Date: Tue, 20 Jan 2026 11:55:52 -0500 Subject: [PATCH 7/9] Add dependabot.yml --- dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 dependabot.yml diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 00000000..eb4e5a21 --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" From 1391abff576e24ba13075efec8f56f4d28457fa1 Mon Sep 17 00:00:00 2001 From: Paul Beaudoin Date: Tue, 20 Jan 2026 13:20:17 -0500 Subject: [PATCH 8/9] Move dependabot.yml --- dependabot.yml => .github/dependabot.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dependabot.yml => .github/dependabot.yml (100%) diff --git a/dependabot.yml b/.github/dependabot.yml similarity index 100% rename from dependabot.yml rename to .github/dependabot.yml From a1cad1f043883f4f03fd521a936f89bcd462cc4f Mon Sep 17 00:00:00 2001 From: Vera Kahn Date: Wed, 4 Feb 2026 09:53:21 -0500 Subject: [PATCH 9/9] update resources index --- config/qa.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/qa.env b/config/qa.env index a802f3f5..dee23cf2 100644 --- a/config/qa.env +++ b/config/qa.env @@ -1,5 +1,5 @@ ENCRYPTED_ELASTICSEARCH_URI=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJYwgZMGCSqGSIb3DQEHBqCBhTCBggIBADB9BgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDMIkDoQ9C/cCDCAq1wIBEIBQ+L3OgUGeOW9rs1CWkhpBjwM4LbbVRFIWedqew4UXIeSNMJ8cO9SNe4YGCUIoKwCDYt7W7ip3VtDRRRMVvz6QJw+Eg8ugTMVs2pbNFGNvaAQ= -RESOURCES_INDEX=resources-2025-07-07 +ENCRYPTED_RESOURCES_INDEX=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJYwgZMGCSqGSIb3DQEHBqCBhTCBggIBADB9BgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDMIkDoQ9C/cCDCAq1wIBEIBQ+L3OgUGeOW9rs1CWkhpBjwM4LbbVRFIWedqew4UXIeSNMJ8cO9SNe4YGCUIoKwCDYt7W7ip3VtDRRRMVvz6QJw+Eg8ugTMVs2pbNFGNvaAQ= SUBJECTS_INDEX=browse-qa-2026-01-15 ENCRYPTED_ELASTICSEARCH_API_KEY=AQECAHh7ea2tyZ6phZgT4B9BDKwguhlFtRC6hgt+7HbmeFsrsgAAAJ4wgZsGCSqGSIb3DQEHBqCBjTCBigIBADCBhAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAx+kryf2KUmGdBYD9sCARCAV3ygz3eXIdq8JX/wpG9JRWlTNMRcpNE1qT0zNlN4t+ZvXEoedLQa/3p1YjgHw06GIAdA9xtkMV4eH9a1K8uCvjP8XxxNKekcMj59TlResnu9QF3r7pGXuQ==