From f216397c18c44446bf909144499f6508570a9478 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 15 Feb 2023 11:36:04 +0100 Subject: [PATCH 001/262] Change word2vec format to gensim KeyedVector format --- backend/wordmodels/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/wordmodels/utils.py b/backend/wordmodels/utils.py index 6ac669bfb..dc7d6470a 100644 --- a/backend/wordmodels/utils.py +++ b/backend/wordmodels/utils.py @@ -14,25 +14,25 @@ def load_word_models(corpus, binned=False): if type(corpus)==str: corpus = load_corpus(corpus) - w2v_list = glob('{}/*.w2v'.format(corpus.word_model_path)) - full_model = next((item for item in w2v_list if item.endswith('full.w2v')), None) + wv_list = glob('{}/*.wv'.format(corpus.word_model_path)) + full_model = next((item for item in wv_list if item.endswith('full.wv')), None) try: - w2v_list.remove(full_model) + wv_list.remove(full_model) except: raise(Exception("No full word model found for this corpus.")) if binned: - w2v_list.sort() + wv_list.sort() wm = [ { "start_year": get_year(wm_file, 1), "end_year": get_year(wm_file, 2), - "matrix": KeyedVectors.load_word2vec_format(wm_file, binary=True), + "matrix": KeyedVectors.load(wm_file), "vocab": get_vocab(wm_file) } - for wm_file in w2v_list + for wm_file in wv_list ] else: - model = KeyedVectors.load_word2vec_format(full_model, binary=True) + model = KeyedVectors.load(full_model) wm = { "start_year": get_year(full_model, 1), "end_year": get_year(full_model, 2), From 7587647d950990933c003fedfc863db1582e657f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 15 Feb 2023 16:39:11 +0100 Subject: [PATCH 002/262] use KeyedVectors and their vocab throughout --- backend/wordmodels/similarity.py | 33 ++++++++++------------ backend/wordmodels/tests/test_wm_import.py | 8 +++--- backend/wordmodels/utils.py | 16 +++-------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/backend/wordmodels/similarity.py b/backend/wordmodels/similarity.py index 04664b5f5..2c24214c8 100644 --- a/backend/wordmodels/similarity.py +++ b/backend/wordmodels/similarity.py @@ -4,50 +4,47 @@ def term_similarity(wm, term1, term2): - matrix = wm['matrix'] + vectors = wm['vectors'] transformed1 = transform_query(term1) transformed2 = transform_query(term2) - vocab = wm['vocab'] + vocab = vectors.index_to_key if transformed1 in vocab and transformed2 in vocab: - similarity = matrix.similarity(transformed1, transformed2) + similarity = vectors.similarity(transformed1, transformed2) return float(similarity) def find_n_most_similar(wm, query_term, n): - """given a matrix of svd_ppmi or word2vec values + """given vectors of svd_ppmi or word2vec values with its vocabulary and analyzer, determine which n terms match the given query term best """ - vocab = wm['vocab'] transformed_query = transform_query(query_term) - matrix = wm['matrix'] - results = most_similar_items(matrix, vocab, transformed_query, n) + vectors = wm['vectors'] + vocab = vectors.index_to_key + results = most_similar_items(vectors, transformed_query, n) return [{ 'key': result[0], 'similarity': result[1] } for result in results] -def most_similar_items(matrix, vocab, term, n, missing_terms = 0): +def most_similar_items(vectors, term, n, missing_terms = 0): ''' Find the n most similar terms in a keyed vectors matrix, while filtering on the vocabulary. parameters: - - `matrix`: the KeyedVectors matrix - - `vocab`: the vocabulary for the model. This may be a subst of the keys in `matrix`, so - results will be filtered on vocab. + - `vectors`: the KeyedVectors - `term`: the term for which to find the nearest neighbours. Should already have been passed through the model's analyzer. - `n`: number of neighbours to return - `missing_terms`: used for recursion. indicates that of the `n` nearest vectors, `missing_terms` vectors are not actually included in `vocab`, hence we should request `n + missing_terms` vectors ''' - + vocab = vectors.index_to_key if term in vocab: - results = matrix.most_similar(term, topn=n + missing_terms) - filtered_results = [(key, score) for key, score in results if key in vocab] - results_complete = len(filtered_results) == min(n, len(vocab) - 1) + results = vectors.most_similar(term, topn=n + missing_terms) + results_complete = len(results) == min(n, len(vocab) - 1) if results_complete: - return filtered_results + return results else: - delta = n - len(filtered_results) - return most_similar_items(matrix, vocab, term, n, missing_terms=delta + missing_terms) + delta = n - len(results) + return most_similar_items(vectors, term, n, missing_terms=delta + missing_terms) return [] diff --git a/backend/wordmodels/tests/test_wm_import.py b/backend/wordmodels/tests/test_wm_import.py index 6b9b9cf0a..1fc07954e 100644 --- a/backend/wordmodels/tests/test_wm_import.py +++ b/backend/wordmodels/tests/test_wm_import.py @@ -12,9 +12,9 @@ def test_complete_import(test_app, mock_corpus): model = load_word_models(corpus) assert model - weights = model['matrix'] + weights = model['vectors'] assert weights.vector_size == TEST_DIMENSIONS - vocab = model['vocab'] + vocab = weights.index_to_key assert len(vocab) == TEST_VOCAB_SIZE @@ -29,10 +29,10 @@ def test_binned_import(test_app, mock_corpus): assert model['start_year'] == start_year assert model['end_year'] == end_year - weights = model['matrix'] + weights = model['vectors'] assert weights.vector_size == TEST_DIMENSIONS - vocab = model['vocab'] + vocab = weights.index_to_key assert len(vocab) == TEST_VOCAB_SIZE def test_word_in_model(test_app, mock_corpus): diff --git a/backend/wordmodels/utils.py b/backend/wordmodels/utils.py index dc7d6470a..449baf9e5 100644 --- a/backend/wordmodels/utils.py +++ b/backend/wordmodels/utils.py @@ -26,8 +26,7 @@ def load_word_models(corpus, binned=False): { "start_year": get_year(wm_file, 1), "end_year": get_year(wm_file, 2), - "matrix": KeyedVectors.load(wm_file), - "vocab": get_vocab(wm_file) + "vectors": KeyedVectors.load(wm_file), } for wm_file in wv_list ] @@ -36,25 +35,18 @@ def load_word_models(corpus, binned=False): wm = { "start_year": get_year(full_model, 1), "end_year": get_year(full_model, 2), - "matrix": model, - "vocab": get_vocab(full_model) + "vectors": model, } return wm -def get_vocab(kv_filename): - vocab_name = '{}_vocab.pkl'.format(splitext(kv_filename)[0]) - with open(vocab_name, 'rb') as f: - return pickle.load(f) - def get_year(kv_filename, position): return int(splitext(basename(kv_filename))[0].split('_')[position]) def word_in_model(query_term, corpus, max_distance = 2): model = load_word_models(corpus) - vocab = model['vocab'] transformed_query = transform_query(query_term) - - if transformed_query in model['vocab']: + vocab = model['vectors'].index_to_key + if transformed_query in vocab: return { 'exists': True } else: is_similar = lambda term : damerau_levenshtein(query_term, term) <= max_distance From 828e98de6d9f75d727f19f45923b1a623067db68 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 22 Feb 2023 12:12:00 +0100 Subject: [PATCH 003/262] correct documentation --- documentation/Adding-word-models.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/documentation/Adding-word-models.md b/documentation/Adding-word-models.md index 213490b5a..8d420c70c 100644 --- a/documentation/Adding-word-models.md +++ b/documentation/Adding-word-models.md @@ -4,11 +4,9 @@ Corpora have the option to include word vectors. I-analyzer visualisations are b ## Expected file format Word embeddings are expected to come with the following files: -- `_full.w2v` (contains gensim KeyedVectors for a model trained on the whole time period) -- `_full_vocab.pkl` (contains a list of terms present in the word vectors of the whole time period) +- `_full.wv` (contains gensim KeyedVectors for a model trained on the whole time period) For each time bin, it expects files of the format -- `_{startYear}_{endYear}.w2v` (contains gensim KeyedVectors for a model trained on the time bin) -- `_{startYear}_{endYear}_vocab.pkl` (contains a list of terms present in the word vectors of the time bin) +- `_{startYear}_{endYear}.wv` (contains gensim KeyedVectors for a model trained on the time bin) ## Documentation Please include documentation on the method and settings used to train a model. This documentation is expected to be located in `wm/documentation.md`, next to the corpus definition that includes word models. From 212b00ac6000cf3a1ddf78d796c9236ac4dc2e19 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 19 Apr 2023 17:05:50 +0200 Subject: [PATCH 004/262] extract corpus selector to own component --- frontend/src/app/app.module.ts | 2 + .../corpus-selection.component.html | 27 +----------- .../corpus-selection.component.scss | 42 ------------------- .../corpus-selection.component.ts | 15 +------ .../corpus-selector.component.html | 26 ++++++++++++ .../corpus-selector.component.scss | 42 +++++++++++++++++++ .../corpus-selector.component.spec.ts | 25 +++++++++++ .../corpus-selector.component.ts | 29 +++++++++++++ 8 files changed, 126 insertions(+), 82 deletions(-) create mode 100644 frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html create mode 100644 frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss create mode 100644 frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.spec.ts create mode 100644 frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index da0812bc6..6123e8a38 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -88,6 +88,7 @@ import { DownloadOptionsComponent } from './download/download-options/download-o import { JoyplotComponent } from './visualization/ngram/joyplot/joyplot.component'; import { VerifyEmailComponent } from './login/verify-email/verify-email.component'; import { DocumentPageComponent } from './document-page/document-page.component'; +import { CorpusSelectorComponent } from './corpus-selection/corpus-selector/corpus-selector.component'; export const appRoutes: Routes = [ @@ -171,6 +172,7 @@ export const declarations: any[] = [ BooleanFilterComponent, CorpusHeaderComponent, CorpusSelectionComponent, + CorpusSelectorComponent, DateFilterComponent, DialogComponent, DocumentPageComponent, diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.html b/frontend/src/app/corpus-selection/corpus-selection.component.html index 1b3d777af..41c454ec6 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.html +++ b/frontend/src/app/corpus-selection/corpus-selection.component.html @@ -10,32 +10,7 @@

diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.scss b/frontend/src/app/corpus-selection/corpus-selection.component.scss index 6c5380af9..4e77e91c4 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.scss +++ b/frontend/src/app/corpus-selection/corpus-selection.component.scss @@ -1,46 +1,4 @@ -@import "../../_utilities"; - -.card { - cursor: pointer; -} - -.card-content { - padding: 1.5rem; - color: $text-primary-color; - text-decoration: none; - background-color: $primary; -} - -.card-info-icon { - color: white; - font-size: medium; -} - -.card-footer { - border-top: 1px solid $contrast-primary-color; -} - -.card-footer-item { - border-top: 1px solid $contrast-primary-color; - color: $text-primary-color; - cursor: pointer; - text-decoration: none; - background-color: $contrast-primary-color; - - &:hover{ - color: $text-primary-color; - } -} - -.title-content { - font-size: 1.5em; -} - .subtitle { margin-top: 1.5em !important; } -.moreInfoLink { - background: none; - border: none; -} diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.ts b/frontend/src/app/corpus-selection/corpus-selection.component.ts index 63b570b58..51b50b62e 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selection.component.ts @@ -1,9 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; import { Corpus } from '../models/corpus'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { DialogService } from '../services/dialog.service'; @Component({ selector: 'ia-corpus-selection', @@ -14,18 +11,8 @@ export class CorpusSelectionComponent implements OnInit { @Input() public items: Corpus[]; - constructor(private router: Router, private domSanitizer: DomSanitizer, private dialogService: DialogService) { } + constructor() { } ngOnInit() { } - - showMoreInfo(corpus: Corpus): void { - this.dialogService.showDescriptionPage(corpus); - } - - navigateToCorpus(event: any, corpusName: string): void { - if (!event.target.classList.contains('moreInfoLink')) { - this.router.navigate(['/search', corpusName]); - } - } } diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html new file mode 100644 index 000000000..ddd31c8ed --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss new file mode 100644 index 000000000..72ce57874 --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss @@ -0,0 +1,42 @@ +@import "../../../_utilities"; + +.card { + cursor: pointer; +} + +.card-content { + padding: 1.5rem; + color: $text-primary-color; + text-decoration: none; + background-color: $primary; +} + +.card-info-icon { + color: white; + font-size: medium; +} + +.card-footer { + border-top: 1px solid $contrast-primary-color; +} + +.card-footer-item { + border-top: 1px solid $contrast-primary-color; + color: $text-primary-color; + cursor: pointer; + text-decoration: none; + background-color: $contrast-primary-color; + + &:hover { + color: $text-primary-color; + } +} + +.title-content { + font-size: 1.5em; +} + +.moreInfoLink { + background: none; + border: none; +} diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.spec.ts b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.spec.ts new file mode 100644 index 000000000..c2815f06e --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { CorpusSelectorComponent } from './corpus-selector.component'; +import { commonTestBed } from '../../common-test-bed'; +import { mockCorpus } from '../../../mock-data/corpus'; + +describe('CorpusSelectorComponent', () => { + let component: CorpusSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + commonTestBed().testingModule.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CorpusSelectorComponent); + component = fixture.componentInstance; + component.corpus = mockCorpus; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts new file mode 100644 index 000000000..559329ee4 --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts @@ -0,0 +1,29 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Corpus } from '../../models'; +import { DialogService } from '../../services'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'ia-corpus-selector', + templateUrl: './corpus-selector.component.html', + styleUrls: ['./corpus-selector.component.scss'] +}) +export class CorpusSelectorComponent implements OnInit { + @Input() corpus: Corpus; + + constructor(private dialogService: DialogService, private router: Router) { } + + ngOnInit(): void { + } + + showMoreInfo(): void { + this.dialogService.showDescriptionPage(this.corpus); + } + + navigateToCorpus(event: any): void { + if (!event.target.classList.contains('moreInfoLink')) { + this.router.navigate(['/search', this.corpus.name]); + } + } + +} From 2ded57df55b6b18ccdc0cea688be7c6f82257dd3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 19 Apr 2023 17:28:00 +0200 Subject: [PATCH 005/262] placeholder for corpus metatdata --- .../corpus-selector/corpus-selector.component.html | 7 +++++++ .../corpus-selector/corpus-selector.component.ts | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html index ddd31c8ed..4ad3937c9 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -16,6 +16,13 @@

{{corpus.description}}
+ +
+

Language: Dutch

+

Type: Parliamentary debates

+

Date: {{minYear}}-{{maxYear}}

+
+

+
+ -
-

Language: Dutch

-

Type: Parliamentary debates

-

Date: {{minYear}}-{{maxYear}}

+
+ +
+
+ {{corpus.description}} +
+ +
- - + diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss index 72ce57874..b04fbb6a3 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss @@ -4,24 +4,52 @@ cursor: pointer; } -.card-content { - padding: 1.5rem; +.corpus-container { + padding: 0; color: $text-primary-color; text-decoration: none; background-color: $primary; + + > .column { + // compensate for lack of padding in corpus-container + padding-top: 20px; + padding-bottom: 20px; + } +} + + +.image-column { + position: relative; + margin-right: 10px; + overflow-y: hidden; } -.card-info-icon { +.corpus-image { + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + + img { + height: 100%; + width: 100%; + object-fit: cover; + object-position: center; + } +} + + +.info-icon { color: white; - font-size: medium; } .card-footer { border-top: 1px solid $contrast-primary-color; } -.card-footer-item { - border-top: 1px solid $contrast-primary-color; +.corpus-action { + border: 1px solid $contrast-primary-color; color: $text-primary-color; cursor: pointer; text-decoration: none; @@ -32,11 +60,31 @@ } } +.title-row { + margin-bottom: 0px; +} + +.title-divider { + margin-top: 0px; + margin-bottom: 1rem; +} + .title-content { font-size: 1.5em; + cursor: pointer; + color: white !important; } .moreInfoLink { background: none; border: none; + font-size: 1.25em; +} + +.columns .align-bottom { + align-items: end; +} + +strong { + color: inherit; } diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts index 6a7166e0a..17524d51f 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { Corpus } from '../../models'; import { DialogService } from '../../services'; import { Router } from '@angular/router'; +import { faInfoCircle, faSearch } from '@fortawesome/free-solid-svg-icons'; @Component({ selector: 'ia-corpus-selector', @@ -11,6 +12,9 @@ import { Router } from '@angular/router'; export class CorpusSelectorComponent implements OnInit { @Input() corpus: Corpus; + infoIcon = faInfoCircle; + searchIcon = faSearch; + constructor(private dialogService: DialogService, private router: Router) { } get minYear() { From cea8a41580a4c53c85e735de921edb319568ee4b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 24 Apr 2023 13:55:48 +0200 Subject: [PATCH 007/262] scaffold mock filter form --- .../corpus-selection.component.html | 28 +++++++++++++++++++ .../corpus-selection.component.scss | 3 ++ .../corpus-selection.component.ts | 3 ++ 3 files changed, 34 insertions(+) diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.html b/frontend/src/app/corpus-selection/corpus-selection.component.html index db7103739..d9d1aed85 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.html +++ b/frontend/src/app/corpus-selection/corpus-selection.component.html @@ -7,6 +7,34 @@

Select a corpus to search through


+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.scss b/frontend/src/app/corpus-selection/corpus-selection.component.scss index 4e77e91c4..832d7c30c 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.scss +++ b/frontend/src/app/corpus-selection/corpus-selection.component.scss @@ -2,3 +2,6 @@ margin-top: 1.5em !important; } +.year-input { + width: 6em; +} diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.ts b/frontend/src/app/corpus-selection/corpus-selection.component.ts index 51b50b62e..2122cea34 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selection.component.ts @@ -11,6 +11,9 @@ export class CorpusSelectionComponent implements OnInit { @Input() public items: Corpus[]; + minDate = new Date('01/01/1800'); + maxDate = new Date(Date.now()); + constructor() { } ngOnInit() { From 58248990516ce9ea568342e812361ea4467d61c3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 13:51:43 +0200 Subject: [PATCH 008/262] add tests for untested corpora --- .../test_dutchannualreports.py | 13 ++++++++++ .../dutchnewspapers/test_dutchnewspapers.py | 24 +++++++++++++++++++ backend/corpora/ecco/test_ecco.py | 13 ++++++++++ backend/corpora/goodreads/test_goodreads.py | 13 ++++++++++ .../guardianobserver/test_guardianobserver.py | 13 ++++++++++ .../test_jewishinscriptions.py | 13 ++++++++++ .../corpora/periodicals/test_periodicals.py | 13 ++++++++++ backend/corpora/times/test_times.py | 13 ++++++++++ backend/corpora/troonredes/test_troonredes.py | 13 ++++++++++ backend/corpora/utils_test.py | 16 +++++++++++++ 10 files changed, 144 insertions(+) create mode 100644 backend/corpora/dutchannualreports/test_dutchannualreports.py create mode 100644 backend/corpora/dutchnewspapers/test_dutchnewspapers.py create mode 100644 backend/corpora/ecco/test_ecco.py create mode 100644 backend/corpora/goodreads/test_goodreads.py create mode 100644 backend/corpora/guardianobserver/test_guardianobserver.py create mode 100644 backend/corpora/jewishinscriptions/test_jewishinscriptions.py create mode 100644 backend/corpora/periodicals/test_periodicals.py create mode 100644 backend/corpora/times/test_times.py create mode 100644 backend/corpora/troonredes/test_troonredes.py create mode 100644 backend/corpora/utils_test.py diff --git a/backend/corpora/dutchannualreports/test_dutchannualreports.py b/backend/corpora/dutchannualreports/test_dutchannualreports.py new file mode 100644 index 000000000..523b4bbe4 --- /dev/null +++ b/backend/corpora/dutchannualreports/test_dutchannualreports.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_dutchannualreports(settings, admin_client): + settings.CORPORA = { + 'dutchannualreports': os.path.join(here, 'dutchannualreports.py') + } + settings.DUTCHANNUALREPORTS_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Dutch Annual Reports' diff --git a/backend/corpora/dutchnewspapers/test_dutchnewspapers.py b/backend/corpora/dutchnewspapers/test_dutchnewspapers.py new file mode 100644 index 000000000..c3a337863 --- /dev/null +++ b/backend/corpora/dutchnewspapers/test_dutchnewspapers.py @@ -0,0 +1,24 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_dutchnewspapers_public(settings, admin_client): + settings.CORPORA = { + 'dutchnewspapers-public': os.path.join(here, 'dutchnewspapers_public.py') + } + settings.DUTCHNEWSPAPERS_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Public Dutch Newspapers' + +def test_dutchnewspapers_all(settings, admin_client): + settings.CORPORA = { + 'dutchnewspapers-all': os.path.join(here, 'dutchnewspapers_all.py'), + 'dutchnewspapers-public': os.path.join(here, 'dutchnewspapers_public.py') + } + settings.DUTCHNEWSPAPERS_DATA = '' + settings.DUTCHNEWSPAPERS_ALL_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Dutch Newspapers (Delpher)' diff --git a/backend/corpora/ecco/test_ecco.py b/backend/corpora/ecco/test_ecco.py new file mode 100644 index 000000000..25f0b70f0 --- /dev/null +++ b/backend/corpora/ecco/test_ecco.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_ecco(settings, admin_client): + settings.CORPORA = { + 'ecco': os.path.join(here, 'ecco.py') + } + settings.ECCO_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Eighteenth Century Collections Online' diff --git a/backend/corpora/goodreads/test_goodreads.py b/backend/corpora/goodreads/test_goodreads.py new file mode 100644 index 000000000..6746efa0b --- /dev/null +++ b/backend/corpora/goodreads/test_goodreads.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_goodreads(settings, admin_client): + settings.CORPORA = { + 'goodreads': os.path.join(here, 'goodreads.py') + } + settings.GOODREADS_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'DIOPTRA-L' diff --git a/backend/corpora/guardianobserver/test_guardianobserver.py b/backend/corpora/guardianobserver/test_guardianobserver.py new file mode 100644 index 000000000..756c63452 --- /dev/null +++ b/backend/corpora/guardianobserver/test_guardianobserver.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_guardian_observer(settings, admin_client): + settings.CORPORA = { + 'guardian-observer': os.path.join(here, 'guardianobserver.py') + } + settings.GO_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Guardian-Observer' diff --git a/backend/corpora/jewishinscriptions/test_jewishinscriptions.py b/backend/corpora/jewishinscriptions/test_jewishinscriptions.py new file mode 100644 index 000000000..e4a2b54b7 --- /dev/null +++ b/backend/corpora/jewishinscriptions/test_jewishinscriptions.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_jewish_inscriptions(settings, admin_client): + settings.CORPORA = { + 'jewish-inscriptions': os.path.join(here, 'jewishinscriptions.py') + } + settings.JEWISH_INSCRIPTIONS_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Jewish Funerary Inscriptions' diff --git a/backend/corpora/periodicals/test_periodicals.py b/backend/corpora/periodicals/test_periodicals.py new file mode 100644 index 000000000..f43f88d4c --- /dev/null +++ b/backend/corpora/periodicals/test_periodicals.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_periodicals(settings, admin_client): + settings.CORPORA = { + 'periodicals': os.path.join(here, 'periodicals.py') + } + settings.PERIODICALS_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Periodicals' diff --git a/backend/corpora/times/test_times.py b/backend/corpora/times/test_times.py new file mode 100644 index 000000000..62a5b86cb --- /dev/null +++ b/backend/corpora/times/test_times.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_times(settings, admin_client): + settings.CORPORA = { + 'times': os.path.join(here, 'times.py') + } + settings.TIMES_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Times' diff --git a/backend/corpora/troonredes/test_troonredes.py b/backend/corpora/troonredes/test_troonredes.py new file mode 100644 index 000000000..314e9faa3 --- /dev/null +++ b/backend/corpora/troonredes/test_troonredes.py @@ -0,0 +1,13 @@ +import os +from corpora.utils_test import corpus_from_api + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_troonredes(settings, admin_client): + settings.CORPORA = { + 'troonredes': os.path.join(here, 'troonredes.py') + } + settings.TROONREDES_DATA = '' + + corpus = corpus_from_api(admin_client) + assert corpus['title'] == 'Troonredes' diff --git a/backend/corpora/utils_test.py b/backend/corpora/utils_test.py new file mode 100644 index 000000000..db49b3681 --- /dev/null +++ b/backend/corpora/utils_test.py @@ -0,0 +1,16 @@ +def corpus_from_api(client): + ''' + Try fetching a corpus through the API. + + Used for testing that a corpus definition can be used + without syntax/runtime errors. + + Returns the serialised version of the first corpus. Most + useful when you have configured your settings with only one corpus. + ''' + + response = client.get('/api/corpus/') + assert response.status_code == 200 + assert len(response.data) + corpus = response.data[0] + return corpus From 74fe476c7c07b4c2afb703a175d57a017dbcd020 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 11:24:04 +0200 Subject: [PATCH 009/262] add category and language to corpus class --- backend/addcorpus/constants.py | 49 ++++++++++++++++++++ backend/addcorpus/corpus.py | 39 +++++++++++++++- backend/addcorpus/tests/mock_csv_corpus.py | 3 ++ backend/addcorpus/tests/test_corpus_views.py | 12 ++++- backend/corpora/parliament/netherlands.py | 2 + 5 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 backend/addcorpus/constants.py diff --git a/backend/addcorpus/constants.py b/backend/addcorpus/constants.py new file mode 100644 index 000000000..484574d48 --- /dev/null +++ b/backend/addcorpus/constants.py @@ -0,0 +1,49 @@ +''' +Language options for corpora. +''' +LANGUAGES = [ + ('arabic', 'Arabic'), + ('azerbaijani', 'Azerbaijani'), + ('basque', 'Basque'), + ('bengali', 'Bengali'), + ('catalan', 'Catalan'), + ('chinese', 'Chinese'), + ('danish', 'Danish'), + ('dutch', 'Dutch'), + ('english', 'English'), + ('finnish', 'Finnish'), + ('french', 'French'), + ('german', 'German'), + ('greek', 'Greek'), + ('hebrew', 'Hebrew'), + ('hinglish', 'Hinglish'), + ('hungarian', 'Hungarian'), + ('indonesian', 'Indonesian'), + ('italian', 'Italian'), + ('kazakh', 'Kazakh'), + ('nepali', 'Nepali'), + ('norwegian', 'Norwegian'), + ('portuguese'), + ('romanian', 'Romanian'), + ('russian', 'Russian'), + ('slovene', 'Slovene'), + ('spanish', 'Spanish'), + ('swedish', 'Swedish'), + ('tajik', 'Tajik'), + ('turkish', 'Turkish'), +] + +''' +Types of data +''' +CATEGORIES = [ + ('newspaper', 'Newspapers'), + ('parliament', 'Parliamentary debates'), + ('periodical', 'Periodicals'), + ('finance', 'Financial reports'), + ('ruling', 'Court rulings'), + ('review', 'Online reviews'), + ('inscription', 'Funerary inscriptions'), + ('oration', 'Orations'), + ('book', 'Books'), +] diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index b38e0005a..4727cf760 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -14,7 +14,7 @@ from os.path import isdir import logging logger = logging.getLogger('indexing') -import os +from addcorpus.constants import LANGUAGES, CATEGORIES class Corpus(object): ''' @@ -61,6 +61,29 @@ def max_date(self): ''' raise NotImplementedError() + @property + def languages(self): + ''' + Language(s) used in the corpus + + Should be a list of strings. Each language should + correspond to an item in addcorpus.constants.LANGUAGES, so it + can be serialised + ''' + if hasattr(self, 'language'): + return [self.language] + else: + raise NotImplementedError + + @property + def category(self): + ''' + Type of documents in the corpus + + See addcorpus.constants.CATEGORIES for options + ''' + raise NotImplementedError() + @property def es_index(self): ''' @@ -249,6 +272,10 @@ def serialize(self): for field in self.fields: field_list.append(field.serialize()) corpus_dict[ca[0]] = field_list + elif ca[0] == 'languages': + corpus_dict[ca[0]] = [self._format_option(language, LANGUAGES) for language in ca[1]] + elif ca[0] == 'category': + corpus_dict[ca[0]] = self._format_option(ca[1], CATEGORIES) elif type(ca[1]) == datetime: timedict = {'year': ca[1].year, 'month': ca[1].month, @@ -260,6 +287,16 @@ def serialize(self): corpus_dict[ca[0]] = ca[1] return corpus_dict + def _format_option(self, value, options): + ''' + For serialisation: format language or category based on list of options + ''' + return next( + nice_string + for code, nice_string in options + if value == code + ) + def sources(self, start=datetime.min, end=datetime.max): ''' Obtain source files for the corpus, relevant to the given timespan. diff --git a/backend/addcorpus/tests/mock_csv_corpus.py b/backend/addcorpus/tests/mock_csv_corpus.py index 2d22a3190..856233313 100644 --- a/backend/addcorpus/tests/mock_csv_corpus.py +++ b/backend/addcorpus/tests/mock_csv_corpus.py @@ -17,6 +17,9 @@ class MockCSVCorpus(CSVCorpus): data_directory = os.path.join(here, 'csv_example') field_entry = 'character' + language = 'english' + category = 'book' + def sources(self, start, end): for filename in os.listdir(self.data_directory): full_path = os.path.join(self.data_directory, filename) diff --git a/backend/addcorpus/tests/test_corpus_views.py b/backend/addcorpus/tests/test_corpus_views.py index e79bccf0e..aefe8f654 100644 --- a/backend/addcorpus/tests/test_corpus_views.py +++ b/backend/addcorpus/tests/test_corpus_views.py @@ -1,7 +1,6 @@ -from rest_framework.test import APIClient from rest_framework import status from users.models import CustomUser - +from addcorpus.tests.mock_csv_corpus import MockCSVCorpus def test_no_corpora(db, settings, auth_client): settings.CORPORA = {} @@ -31,3 +30,12 @@ def test_no_corpus_access(client, mock_corpus, mock_corpus_user): client.force_login(user) response = client.get(f'/api/corpus/documentation/{mock_corpus}/mock-csv-corpus.md') assert response.status_code == 403 + +def test_corpus_serialization(client, mock_corpus, mock_corpus_user): + client.force_login(mock_corpus_user) + response = client.get('/api/corpus/') + corpus = response.data[0] + assert corpus['title'] == MockCSVCorpus.title + assert corpus['languages'] == ['English'] + assert corpus['category'] == 'Books' + assert len(corpus['fields']) == 2 diff --git a/backend/corpora/parliament/netherlands.py b/backend/corpora/parliament/netherlands.py index 472c31d79..4bb0ecb5c 100644 --- a/backend/corpora/parliament/netherlands.py +++ b/backend/corpora/parliament/netherlands.py @@ -136,6 +136,8 @@ class ParliamentNetherlands(Parliament, XMLCorpus): tag_entry = lambda _, metadata: 'speech' if is_old(metadata) else 'u' language = 'dutch' + category = 'parliament' + def sources(self, start, end): logger = logging.getLogger(__name__) From 79a83bd63acee4e9429300a566122e68bc11232e Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 11:48:35 +0200 Subject: [PATCH 010/262] add languages and categories to existing corpora --- backend/addcorpus/constants.py | 9 ++++++++- backend/corpora/dutchannualreports/dutchannualreports.py | 3 +++ .../corpora/dutchnewspapers/dutchnewspapers_public.py | 2 ++ backend/corpora/ecco/ecco.py | 2 ++ backend/corpora/goodreads/goodreads.py | 2 ++ backend/corpora/guardianobserver/guardianobserver.py | 2 ++ backend/corpora/jewishinscriptions/jewishinscriptions.py | 2 ++ backend/corpora/parliament/ireland.py | 1 + backend/corpora/parliament/parliament.py | 1 + backend/corpora/periodicals/periodicals.py | 2 ++ backend/corpora/rechtspraak/rechtspraak.py | 2 ++ backend/corpora/times/times.py | 2 ++ backend/corpora/troonredes/troonredes.py | 2 ++ 13 files changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/addcorpus/constants.py b/backend/addcorpus/constants.py index 484574d48..6d7349dc8 100644 --- a/backend/addcorpus/constants.py +++ b/backend/addcorpus/constants.py @@ -2,6 +2,7 @@ Language options for corpora. ''' LANGUAGES = [ + ('afrikaans', 'Afrikaans'), ('arabic', 'Arabic'), ('azerbaijani', 'Azerbaijani'), ('basque', 'Basque'), @@ -13,17 +14,20 @@ ('english', 'English'), ('finnish', 'Finnish'), ('french', 'French'), + ('gaelic', 'Gaelic'), ('german', 'German'), ('greek', 'Greek'), ('hebrew', 'Hebrew'), ('hinglish', 'Hinglish'), ('hungarian', 'Hungarian'), ('indonesian', 'Indonesian'), + ('irish', 'Irish'), ('italian', 'Italian'), ('kazakh', 'Kazakh'), + ('latin', 'Latin'), ('nepali', 'Nepali'), ('norwegian', 'Norwegian'), - ('portuguese'), + ('portuguese', 'Portuguese'), ('romanian', 'Romanian'), ('russian', 'Russian'), ('slovene', 'Slovene'), @@ -31,6 +35,9 @@ ('swedish', 'Swedish'), ('tajik', 'Tajik'), ('turkish', 'Turkish'), + ('welsh', 'Welsh'), + ('var', 'Various'), + ('', 'Unknown') ] ''' diff --git a/backend/corpora/dutchannualreports/dutchannualreports.py b/backend/corpora/dutchannualreports/dutchannualreports.py index 8e3569593..a4552e41f 100644 --- a/backend/corpora/dutchannualreports/dutchannualreports.py +++ b/backend/corpora/dutchannualreports/dutchannualreports.py @@ -30,6 +30,9 @@ class DutchAnnualReports(XMLCorpus): allow_image_download = getattr(settings, 'DUTCHANNUALREPORTS_ALLOW_IMAGE_DOWNLOAD', True) word_model_path = getattr(settings, 'DUTCHANNUALREPORTS_WM', None) + language = 'dutch' + category = 'finance' + mimetype = 'application/pdf' # Data overrides from .common.XMLCorpus diff --git a/backend/corpora/dutchnewspapers/dutchnewspapers_public.py b/backend/corpora/dutchnewspapers/dutchnewspapers_public.py index 047ed74fd..7d48b0dab 100644 --- a/backend/corpora/dutchnewspapers/dutchnewspapers_public.py +++ b/backend/corpora/dutchnewspapers/dutchnewspapers_public.py @@ -29,6 +29,8 @@ class DutchNewspapersPublic(XMLCorpus): data_directory = settings.DUTCHNEWSPAPERS_DATA es_index = getattr(settings, 'DUTCHNEWSPAPERS_ES_INDEX', 'dutchnewspapers-public') image = 'dutchnewspapers.jpg' + language = 'dutch' + category = 'newspaper' tag_toplevel = 'text' tag_entry = 'p' diff --git a/backend/corpora/ecco/ecco.py b/backend/corpora/ecco/ecco.py index 3366d08d0..040ba34a6 100644 --- a/backend/corpora/ecco/ecco.py +++ b/backend/corpora/ecco/ecco.py @@ -31,6 +31,8 @@ class Ecco(XMLCorpus): image = 'ecco.jpg' scan_image_type = getattr(settings, 'ECCO_SCAN_IMAGE_TYPE', 'application/pdf') es_settings = None + languages = ['english', 'welsh', 'irish', 'gaelic'] # according to gale's documentation + category = 'book' tag_toplevel = 'pageContent' tag_entry = 'page' diff --git a/backend/corpora/goodreads/goodreads.py b/backend/corpora/goodreads/goodreads.py index 3df78abe2..96bacf5c9 100644 --- a/backend/corpora/goodreads/goodreads.py +++ b/backend/corpora/goodreads/goodreads.py @@ -31,6 +31,8 @@ class GoodReads(CSVCorpus): image = 'DioptraL.png' description_page = 'goodreads.md' visualize = [] + languages = ['english', 'spanish', 'italian', 'portuguese', 'french', 'dutch', 'german', 'arabic', 'afrikaans', 'swedish', 'var'] # languages with > 1000 docs + category = 'review' # New data members non_xml_msg = 'Skipping non-XML file {}' diff --git a/backend/corpora/guardianobserver/guardianobserver.py b/backend/corpora/guardianobserver/guardianobserver.py index dce3444c8..37048bea0 100644 --- a/backend/corpora/guardianobserver/guardianobserver.py +++ b/backend/corpora/guardianobserver/guardianobserver.py @@ -37,6 +37,8 @@ class GuardianObserver(XMLCorpus): es_index = getattr(settings, 'GO_ES_INDEX', 'guardianobserver') image = 'guardianobserver.jpg' scan_image_type = getattr(settings, 'GO_SCAN_IMAGE_TYPE', 'application/pdf') + language = 'english' + category = 'newspaper' tag_toplevel = 'Record' diff --git a/backend/corpora/jewishinscriptions/jewishinscriptions.py b/backend/corpora/jewishinscriptions/jewishinscriptions.py index 204deb90b..193596003 100644 --- a/backend/corpora/jewishinscriptions/jewishinscriptions.py +++ b/backend/corpora/jewishinscriptions/jewishinscriptions.py @@ -23,6 +23,8 @@ class JewishInscriptions(XMLCorpus): es_index = getattr(settings, 'JEWISH_INSCRIPTIONS_ES_INDEX', 'jewishinscriptions') image = 'jewish_inscriptions.jpg' visualize = [] + languages = ['hebrew', 'latin'] + category = 'inscription' # Data overrides from .common.XMLCorpus tag_toplevel = '' diff --git a/backend/corpora/parliament/ireland.py b/backend/corpora/parliament/ireland.py index 7462e9ea2..a60735a47 100644 --- a/backend/corpora/parliament/ireland.py +++ b/backend/corpora/parliament/ireland.py @@ -451,6 +451,7 @@ class ParliamentIreland(Parliament, Corpus): description_page = 'ireland.md' language = None # corpus uses multiple languages, so we will not be using language-specific analyzers es_settings = {'index': {'number_of_replicas': 0}} # do not include analyzers in es_settings + languages = ['english', 'irish'] @property def subcorpora(self): diff --git a/backend/corpora/parliament/parliament.py b/backend/corpora/parliament/parliament.py index 7aca87c33..275c01645 100644 --- a/backend/corpora/parliament/parliament.py +++ b/backend/corpora/parliament/parliament.py @@ -35,6 +35,7 @@ class Parliament(Corpus): data_directory = 'bogus' language = 'english' + category = 'parliament' @property def es_settings(self): diff --git a/backend/corpora/periodicals/periodicals.py b/backend/corpora/periodicals/periodicals.py index 1572ebf13..37460fbe8 100644 --- a/backend/corpora/periodicals/periodicals.py +++ b/backend/corpora/periodicals/periodicals.py @@ -33,6 +33,8 @@ class Periodicals(XMLCorpus): image = 'Fleet_Street.jpg' scan_image_type = getattr(settings, 'PERIODICALS_SCAN_IMAGE_TYPE', 'image/jpeg') description_page = '19thCenturyUKPeriodicals.md' + language = 'english' + category = 'periodical' tag_toplevel = 'articles' tag_entry = 'artInfo' diff --git a/backend/corpora/rechtspraak/rechtspraak.py b/backend/corpora/rechtspraak/rechtspraak.py index bc897ba45..35a4878ca 100644 --- a/backend/corpora/rechtspraak/rechtspraak.py +++ b/backend/corpora/rechtspraak/rechtspraak.py @@ -37,6 +37,8 @@ class Rechtspraak(XMLCorpus): es_index = getattr(settings, 'RECHTSPRAAK_ES_INDEX', 'rechtspraak') image = 'rechtszaal.jpeg' toplevel_zip_file = 'OpenDataUitspraken.zip' + language = 'dutch' + category = 'ruling' tag_toplevel = 'open-rechtspraak' diff --git a/backend/corpora/times/times.py b/backend/corpora/times/times.py index 1218e3db5..f3ff50322 100644 --- a/backend/corpora/times/times.py +++ b/backend/corpora/times/times.py @@ -32,6 +32,8 @@ class Times(XMLCorpus): image = 'times.jpg' scan_image_type = getattr(settings, 'TIMES_SCAN_IMAGE_TYPE', 'image/png') description_page = 'times.md' + language = 'english' + category = 'newspaper' tag_toplevel = 'issue' tag_entry = 'article' diff --git a/backend/corpora/troonredes/troonredes.py b/backend/corpora/troonredes/troonredes.py index 6ea96ebbb..c33c24e7a 100644 --- a/backend/corpora/troonredes/troonredes.py +++ b/backend/corpora/troonredes/troonredes.py @@ -35,6 +35,8 @@ class Troonredes(XMLCorpus): es_index = getattr(settings, 'TROONREDES_ES_INDEX', 'troonredes') image = 'troon.jpg' word_model_path = getattr(settings, 'TROONREDES_WM', None) + language = 'dutch' + category = 'oration' tag_toplevel = 'doc' tag_entry = 'entry' From ef8794ee68ec2d30a1f421c6dd0e8ea62ae54f51 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 14:18:32 +0200 Subject: [PATCH 011/262] add languages and category to frontend corpus model --- frontend/src/app/models/corpus.ts | 2 ++ frontend/src/app/services/corpus.service.spec.ts | 6 +++++- frontend/src/app/services/corpus.service.ts | 4 +++- frontend/src/app/services/elastic-search.service.spec.ts | 2 ++ frontend/src/app/utils/document-context.spec.ts | 4 +++- .../visualization/barchart/histogram.component.spec.ts | 2 ++ frontend/src/mock-data/corpus.ts | 8 ++++++-- 7 files changed, 23 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/models/corpus.ts b/frontend/src/app/models/corpus.ts index 3c3c361dd..ea779501b 100644 --- a/frontend/src/app/models/corpus.ts +++ b/frontend/src/app/models/corpus.ts @@ -24,6 +24,8 @@ export class Corpus implements ElasticSearchIndex { public scan_image_type: string, public allow_image_download: boolean, public word_models_present: boolean, + public languages: string[], + public category: string, public descriptionpage?: string, public documentContext?: DocumentContext, ) { } diff --git a/frontend/src/app/services/corpus.service.spec.ts b/frontend/src/app/services/corpus.service.spec.ts index 73272f7cf..d0f4b6a37 100644 --- a/frontend/src/app/services/corpus.service.spec.ts +++ b/frontend/src/app/services/corpus.service.spec.ts @@ -93,6 +93,8 @@ describe('CorpusService', () => { title: 'Times', description: 'This is a description.', es_index: 'times', + languages: ['English'], + category: 'Tests', fields: [ { description: @@ -295,7 +297,9 @@ describe('CorpusService', () => { '/static/no-image.jpg', 'png', false, - true + true, + ['English'], + 'Tests' ), ]); }); diff --git a/frontend/src/app/services/corpus.service.ts b/frontend/src/app/services/corpus.service.ts index 46ec83410..7dd61a863 100644 --- a/frontend/src/app/services/corpus.service.ts +++ b/frontend/src/app/services/corpus.service.ts @@ -93,8 +93,10 @@ export class CorpusService { data.scan_image_type, data.allow_image_download, data.word_models_present, + data.languages, + data.category, data.description_page, - this.parseDocumentContext(data.document_context, allFields) + this.parseDocumentContext(data.document_context, allFields), ); }; diff --git a/frontend/src/app/services/elastic-search.service.spec.ts b/frontend/src/app/services/elastic-search.service.spec.ts index b4179798f..5e5fed358 100644 --- a/frontend/src/app/services/elastic-search.service.spec.ts +++ b/frontend/src/app/services/elastic-search.service.spec.ts @@ -32,6 +32,8 @@ const mockCorpus: Corpus = { scan_image_type: undefined, allow_image_download: true, word_models_present: false, + languages: ['English'], + category: 'Tests', fields: [ { name: 'content', diff --git a/frontend/src/app/utils/document-context.spec.ts b/frontend/src/app/utils/document-context.spec.ts index 39464b447..26c02dfa5 100644 --- a/frontend/src/app/utils/document-context.spec.ts +++ b/frontend/src/app/utils/document-context.spec.ts @@ -49,7 +49,9 @@ describe('document context utils', () => { mockField2, mockField3, dateField, - ] + ], + languages: ['English'], + category: 'Tests', }; const document: FoundDocument = { diff --git a/frontend/src/app/visualization/barchart/histogram.component.spec.ts b/frontend/src/app/visualization/barchart/histogram.component.spec.ts index 679c2caa1..dc7b20f1b 100644 --- a/frontend/src/app/visualization/barchart/histogram.component.spec.ts +++ b/frontend/src/app/visualization/barchart/histogram.component.spec.ts @@ -17,6 +17,8 @@ const MOCK_CORPUS: Corpus = { scan_image_type: 'nothing', allow_image_download: false, word_models_present: false, + languages: ['English'], + category: 'Tests', fields: [ { name: 'content', diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index 9cc62d2d7..ac6dd372d 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -69,7 +69,9 @@ export const mockCorpus: Corpus = { scan_image_type: 'pdf', allow_image_download: false, word_models_present: false, - fields: [mockField] + fields: [mockField], + languages: ['English'], + category: 'Tests' }; export const mockCorpus2: Corpus = { @@ -84,7 +86,9 @@ export const mockCorpus2: Corpus = { scan_image_type: 'pdf', allow_image_download: false, word_models_present: false, - fields: [mockField2] + fields: [mockField2], + languages: ['English'], + category: 'Tests' }; export class CorpusServiceMock { From 022650075d17f4b2c436ba5656529597b58bf963 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 16:21:04 +0200 Subject: [PATCH 012/262] change languages to iso codes --- backend/addcorpus/constants.py | 87 ++++++++++--------- backend/addcorpus/corpus.py | 5 +- backend/addcorpus/es_settings.py | 19 +++- backend/addcorpus/tests/mock_csv_corpus.py | 2 +- .../dutchannualreports/dutchannualreports.py | 2 +- .../dutchnewspapers/dutchnewspapers_public.py | 2 +- backend/corpora/ecco/ecco.py | 2 +- backend/corpora/goodreads/goodreads.py | 2 +- .../guardianobserver/guardianobserver.py | 2 +- .../jewishinscriptions/jewishinscriptions.py | 2 +- backend/corpora/parliament/canada.py | 2 +- backend/corpora/parliament/denmark-new.py | 2 +- backend/corpora/parliament/denmark.py | 2 +- backend/corpora/parliament/finland.py | 2 +- backend/corpora/parliament/france.py | 2 +- backend/corpora/parliament/germany-new.py | 2 +- backend/corpora/parliament/germany-old.py | 2 +- backend/corpora/parliament/ireland.py | 3 +- backend/corpora/parliament/netherlands.py | 2 +- backend/corpora/parliament/norway-new.py | 2 +- backend/corpora/parliament/norway.py | 2 +- backend/corpora/parliament/parliament.py | 3 +- backend/corpora/parliament/sweden-old.py | 2 +- backend/corpora/parliament/sweden.py | 2 +- backend/corpora/parliament/uk.py | 2 +- backend/corpora/periodicals/periodicals.py | 2 +- backend/corpora/rechtspraak/rechtspraak.py | 2 +- backend/corpora/times/times.py | 2 +- backend/corpora/troonredes/troonredes.py | 2 +- 29 files changed, 89 insertions(+), 76 deletions(-) diff --git a/backend/addcorpus/constants.py b/backend/addcorpus/constants.py index 6d7349dc8..f3c858bbf 100644 --- a/backend/addcorpus/constants.py +++ b/backend/addcorpus/constants.py @@ -1,48 +1,50 @@ -''' -Language options for corpora. -''' LANGUAGES = [ - ('afrikaans', 'Afrikaans'), - ('arabic', 'Arabic'), - ('azerbaijani', 'Azerbaijani'), - ('basque', 'Basque'), - ('bengali', 'Bengali'), - ('catalan', 'Catalan'), - ('chinese', 'Chinese'), - ('danish', 'Danish'), - ('dutch', 'Dutch'), - ('english', 'English'), - ('finnish', 'Finnish'), - ('french', 'French'), - ('gaelic', 'Gaelic'), - ('german', 'German'), - ('greek', 'Greek'), - ('hebrew', 'Hebrew'), - ('hinglish', 'Hinglish'), - ('hungarian', 'Hungarian'), - ('indonesian', 'Indonesian'), - ('irish', 'Irish'), - ('italian', 'Italian'), - ('kazakh', 'Kazakh'), - ('latin', 'Latin'), - ('nepali', 'Nepali'), - ('norwegian', 'Norwegian'), - ('portuguese', 'Portuguese'), - ('romanian', 'Romanian'), - ('russian', 'Russian'), - ('slovene', 'Slovene'), - ('spanish', 'Spanish'), - ('swedish', 'Swedish'), - ('tajik', 'Tajik'), - ('turkish', 'Turkish'), - ('welsh', 'Welsh'), - ('var', 'Various'), - ('', 'Unknown') + ('af', 'Afrikaans'), + ('ar', 'Arabic'), # stopword + stemming support + ('azb', 'Azerbaijani'), # stopword support + ('eu', 'Basque'), # stopword + stemming support + ('bn', 'Bengali'), # stopword + stemming support + ('ca', 'Catalan'), # stopword + stemming support + ('zh', 'Chinese'), # stopword support + ('da', 'Danish'), # stopword + stemming support + ('nl', 'Dutch'), # stopword + stemming support + ('dum', 'Middle Dutch'), + ('odt', 'Old Dutch'), + ('en', 'English'), # stopword + stemming support + ('fi', 'Finnish'), # stopword + stemming support + ('fr', 'French'), # stopword + stemming support + ('gd', 'Gaelic'), + ('de', 'German'), # stopword + stemming support + ('grc', 'Ancient Greek'), + ('el', 'Greek'), # stopword + stemming support + ('he', 'Hebrew'), # stopword support + ('hu', 'Hungarian'), # stopword + stemming support + ('ind', 'Indonesian'), # stopword + stemming support + ('ga', 'Irish'), # stemming support + ('it', 'Italian'), # stopword + stemming support + ('kaz', 'Kazakh'), # stopword support + ('la', 'Latin'), + ('ne', 'Nepali'), # stopword support + ('no', 'Norwegian'), # stopword + stemming supported for bokmål; the key for both is 'norwegian' + ('nob', 'Norwegian (Bokmål)'), + ('nno', 'Norwegian (Nynorsk)'), + ('pt', 'Portuguese'), # stopword + stemming support + ('ro', 'Romanian'), # stopword + stemming support + ('ru', 'Russian'), # stopword + stemming support + ('sl', 'Slovene'), # stopword support + ('es', 'Spanish'), # stopword + stemming support + ('sv', 'Swedish'), # stopword + stemming support + ('tg', 'Tajik'), # stopword support + ('tr', 'Turkish'), # stopword + stemming support + ('cy', 'Welsh'), + ('', 'Unknown'), ] - ''' -Types of data +Language options for corpora. + +Based on https://en.wikipedia.org/wiki/ISO_639-3 ''' + CATEGORIES = [ ('newspaper', 'Newspapers'), ('parliament', 'Parliamentary debates'), @@ -54,3 +56,6 @@ ('oration', 'Orations'), ('book', 'Books'), ] +''' +Types of data +''' diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index 4727cf760..e8e80534c 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -70,10 +70,7 @@ def languages(self): correspond to an item in addcorpus.constants.LANGUAGES, so it can be serialised ''' - if hasattr(self, 'language'): - return [self.language] - else: - raise NotImplementedError + return [''] @property def category(self): diff --git a/backend/addcorpus/es_settings.py b/backend/addcorpus/es_settings.py index 664aa8213..251b26aea 100644 --- a/backend/addcorpus/es_settings.py +++ b/backend/addcorpus/es_settings.py @@ -1,5 +1,6 @@ import nltk import os +from addcorpus.constants import LANGUAGES HERE = os.path.abspath(os.path.dirname(__file__)) NLTK_DATA_PATH = os.path.join(HERE, 'nltk_data') @@ -29,10 +30,21 @@ } } -def get_nltk_stopwords(language): +def get_language_key(language_code): + ''' + Get the nltk stopwords file / elasticsearch stemmer name for a language code + + E.g. 'en' -> 'english' + ''' + + name = next((name for code, name in LANGUAGES if code == language_code), language_code) + return name.lower() + +def get_nltk_stopwords(language_code): nltk.download('stopwords', NLTK_DATA_PATH) stopwords_dir = os.path.join(NLTK_DATA_PATH, 'corpora', 'stopwords') languages = os.listdir(stopwords_dir) + language = get_language_key(language_code) if language in languages: filepath = os.path.join(stopwords_dir, language) @@ -46,7 +58,7 @@ def get_nltk_stopwords(language): def es_settings(language = None, stopword_analyzer = False, stemming_analyzer = False): ''' Make elasticsearch settings json for a corpus index. Options: - - `language`: string with the language of the corpus. Must be specified if you want to use stopword or stemming analysers. + - `language`: string with the language code. See addcorpus.constants for options, and which languages support stopwords/stemming - `stopword_analyzer`: define an analyser that removes stopwords. - `stemming_analyzer`: define an analyser that removes stopwords and performs stemming. ''' @@ -92,9 +104,10 @@ def make_stopword_analyzer(): } def make_stemmer_filter(language): + stemmer_language = get_language_key(language) return { "type": "stemmer", - "language": language + "language": stemmer_language } def make_stemmed_analyzer(): diff --git a/backend/addcorpus/tests/mock_csv_corpus.py b/backend/addcorpus/tests/mock_csv_corpus.py index 856233313..5beaaf337 100644 --- a/backend/addcorpus/tests/mock_csv_corpus.py +++ b/backend/addcorpus/tests/mock_csv_corpus.py @@ -17,7 +17,7 @@ class MockCSVCorpus(CSVCorpus): data_directory = os.path.join(here, 'csv_example') field_entry = 'character' - language = 'english' + languages = ['en'] category = 'book' def sources(self, start, end): diff --git a/backend/corpora/dutchannualreports/dutchannualreports.py b/backend/corpora/dutchannualreports/dutchannualreports.py index a4552e41f..fb882c044 100644 --- a/backend/corpora/dutchannualreports/dutchannualreports.py +++ b/backend/corpora/dutchannualreports/dutchannualreports.py @@ -30,7 +30,7 @@ class DutchAnnualReports(XMLCorpus): allow_image_download = getattr(settings, 'DUTCHANNUALREPORTS_ALLOW_IMAGE_DOWNLOAD', True) word_model_path = getattr(settings, 'DUTCHANNUALREPORTS_WM', None) - language = 'dutch' + languages = ['nl'] category = 'finance' mimetype = 'application/pdf' diff --git a/backend/corpora/dutchnewspapers/dutchnewspapers_public.py b/backend/corpora/dutchnewspapers/dutchnewspapers_public.py index 7d48b0dab..167b59143 100644 --- a/backend/corpora/dutchnewspapers/dutchnewspapers_public.py +++ b/backend/corpora/dutchnewspapers/dutchnewspapers_public.py @@ -29,7 +29,7 @@ class DutchNewspapersPublic(XMLCorpus): data_directory = settings.DUTCHNEWSPAPERS_DATA es_index = getattr(settings, 'DUTCHNEWSPAPERS_ES_INDEX', 'dutchnewspapers-public') image = 'dutchnewspapers.jpg' - language = 'dutch' + languages = ['nl'] category = 'newspaper' tag_toplevel = 'text' diff --git a/backend/corpora/ecco/ecco.py b/backend/corpora/ecco/ecco.py index 040ba34a6..1bff611a3 100644 --- a/backend/corpora/ecco/ecco.py +++ b/backend/corpora/ecco/ecco.py @@ -31,7 +31,7 @@ class Ecco(XMLCorpus): image = 'ecco.jpg' scan_image_type = getattr(settings, 'ECCO_SCAN_IMAGE_TYPE', 'application/pdf') es_settings = None - languages = ['english', 'welsh', 'irish', 'gaelic'] # according to gale's documentation + languages = ['en', 'cy', 'ga', 'gd'] # according to gale's documentation category = 'book' tag_toplevel = 'pageContent' diff --git a/backend/corpora/goodreads/goodreads.py b/backend/corpora/goodreads/goodreads.py index 96bacf5c9..6e865b4b8 100644 --- a/backend/corpora/goodreads/goodreads.py +++ b/backend/corpora/goodreads/goodreads.py @@ -31,7 +31,7 @@ class GoodReads(CSVCorpus): image = 'DioptraL.png' description_page = 'goodreads.md' visualize = [] - languages = ['english', 'spanish', 'italian', 'portuguese', 'french', 'dutch', 'german', 'arabic', 'afrikaans', 'swedish', 'var'] # languages with > 1000 docs + languages = ['en', 'es', 'it', 'pt', 'fr', 'nl', 'de', 'ar', 'af', 'sv', ''] # languages with > 1000 docs category = 'review' # New data members diff --git a/backend/corpora/guardianobserver/guardianobserver.py b/backend/corpora/guardianobserver/guardianobserver.py index 37048bea0..669391a3a 100644 --- a/backend/corpora/guardianobserver/guardianobserver.py +++ b/backend/corpora/guardianobserver/guardianobserver.py @@ -37,7 +37,7 @@ class GuardianObserver(XMLCorpus): es_index = getattr(settings, 'GO_ES_INDEX', 'guardianobserver') image = 'guardianobserver.jpg' scan_image_type = getattr(settings, 'GO_SCAN_IMAGE_TYPE', 'application/pdf') - language = 'english' + languages = ['en'] category = 'newspaper' tag_toplevel = 'Record' diff --git a/backend/corpora/jewishinscriptions/jewishinscriptions.py b/backend/corpora/jewishinscriptions/jewishinscriptions.py index 193596003..1f0e5c716 100644 --- a/backend/corpora/jewishinscriptions/jewishinscriptions.py +++ b/backend/corpora/jewishinscriptions/jewishinscriptions.py @@ -23,7 +23,7 @@ class JewishInscriptions(XMLCorpus): es_index = getattr(settings, 'JEWISH_INSCRIPTIONS_ES_INDEX', 'jewishinscriptions') image = 'jewish_inscriptions.jpg' visualize = [] - languages = ['hebrew', 'latin'] + languages = ['he', 'la'] category = 'inscription' # Data overrides from .common.XMLCorpus diff --git a/backend/corpora/parliament/canada.py b/backend/corpora/parliament/canada.py index 5b78dd21b..b8b0f35ee 100644 --- a/backend/corpora/parliament/canada.py +++ b/backend/corpora/parliament/canada.py @@ -19,7 +19,7 @@ class ParliamentCanada(Parliament, CSVCorpus): data_directory = settings.PP_CANADA_DATA es_index = getattr(settings, 'PP_CANADA_INDEX', 'parliament-canada') image = 'canada.jpeg' - language = 'english' + languages = ['en'] description_page = 'canada.md' field_entry = 'speech_id' required_field = 'content' diff --git a/backend/corpora/parliament/denmark-new.py b/backend/corpora/parliament/denmark-new.py index 9b9c880d3..98b8d05c2 100644 --- a/backend/corpora/parliament/denmark-new.py +++ b/backend/corpora/parliament/denmark-new.py @@ -41,7 +41,7 @@ class ParliamentDenmarkNew(Parliament, CSVCorpus): es_index = getattr(settings, 'PP_DENMARK_NEW_INDEX', 'parliament-denmark-new') image = 'denmark.jpg' description_page = 'denmark-new.md' - language = 'danish' + languages = ['da'] delimiter = '\t' document_context = constants.document_context() document_context['context_fields'] = ['date'] diff --git a/backend/corpora/parliament/denmark.py b/backend/corpora/parliament/denmark.py index 97e38ccb6..30dc14521 100644 --- a/backend/corpora/parliament/denmark.py +++ b/backend/corpora/parliament/denmark.py @@ -41,7 +41,7 @@ class ParliamentDenmark(Parliament, CSVCorpus): image = 'denmark.jpg' description_page = 'denmark.md' - language = 'danish' + languages = ['da'] required_field = 'text' diff --git a/backend/corpora/parliament/finland.py b/backend/corpora/parliament/finland.py index 7225a84a5..2cc610e6a 100644 --- a/backend/corpora/parliament/finland.py +++ b/backend/corpora/parliament/finland.py @@ -62,7 +62,7 @@ def sources(self, start, end): yield xml_file, metadata - language = 'finnish' + languages = ['fi'] description_page = 'finland.md' image = 'finland.jpg' diff --git a/backend/corpora/parliament/france.py b/backend/corpora/parliament/france.py index f32ee403e..7a26652ab 100644 --- a/backend/corpora/parliament/france.py +++ b/backend/corpora/parliament/france.py @@ -18,7 +18,7 @@ class ParliamentFrance(Parliament, CSVCorpus): data_directory = settings.PP_FR_DATA es_index = getattr(settings, 'PP_FR_INDEX', 'parliament-france') image = 'france.jpeg' - language = 'french' + languages = ['fr'] description_page = 'france.md' word_model_path = getattr(settings, 'PP_FR_WM', None) diff --git a/backend/corpora/parliament/germany-new.py b/backend/corpora/parliament/germany-new.py index 2bcf33cbe..3571bd831 100644 --- a/backend/corpora/parliament/germany-new.py +++ b/backend/corpora/parliament/germany-new.py @@ -19,7 +19,7 @@ class ParliamentGermanyNew(Parliament, CSVCorpus): data_directory = settings.PP_GERMANY_NEW_DATA es_index = getattr(settings, 'PP_GERMANY_NEW_INDEX', 'parliament-germany-new') image = 'germany-new.jpeg' - language = 'german' + languages = ['de'] word_model_path = getattr(settings, 'PP_DE_WM', None) field_entry = 'id' diff --git a/backend/corpora/parliament/germany-old.py b/backend/corpora/parliament/germany-old.py index 198164bc2..47c52badd 100644 --- a/backend/corpora/parliament/germany-old.py +++ b/backend/corpora/parliament/germany-old.py @@ -20,7 +20,7 @@ class ParliamentGermanyOld(Parliament, CSVCorpus): data_directory = settings.PP_GERMANY_OLD_DATA es_index = getattr(settings, 'PP_GERMANY_OLD_INDEX', 'parliament-germany-old') image = 'germany-old.jpeg' - language = 'german' + languages = ['de'] word_model_path = getattr(settings, 'PP_DE_WM', None) description_page = 'germany-old.md' diff --git a/backend/corpora/parliament/ireland.py b/backend/corpora/parliament/ireland.py index a60735a47..98bfa99ad 100644 --- a/backend/corpora/parliament/ireland.py +++ b/backend/corpora/parliament/ireland.py @@ -449,9 +449,8 @@ class ParliamentIreland(Parliament, Corpus): es_index = getattr(settings, 'PP_IRELAND_INDEX', 'parliament-ireland') image = 'ireland.png' description_page = 'ireland.md' - language = None # corpus uses multiple languages, so we will not be using language-specific analyzers es_settings = {'index': {'number_of_replicas': 0}} # do not include analyzers in es_settings - languages = ['english', 'irish'] + languages = ['en', 'ga'] @property def subcorpora(self): diff --git a/backend/corpora/parliament/netherlands.py b/backend/corpora/parliament/netherlands.py index 4bb0ecb5c..6a0703f76 100644 --- a/backend/corpora/parliament/netherlands.py +++ b/backend/corpora/parliament/netherlands.py @@ -134,7 +134,7 @@ class ParliamentNetherlands(Parliament, XMLCorpus): description_page = 'netherlands.md' tag_toplevel = lambda _, metadata: 'root' if is_old(metadata) else 'TEI' tag_entry = lambda _, metadata: 'speech' if is_old(metadata) else 'u' - language = 'dutch' + languages = ['nl'] category = 'parliament' diff --git a/backend/corpora/parliament/norway-new.py b/backend/corpora/parliament/norway-new.py index 3fef94d03..fd64ac17a 100644 --- a/backend/corpora/parliament/norway-new.py +++ b/backend/corpora/parliament/norway-new.py @@ -53,7 +53,7 @@ class ParliamentNorwayNew(Parliament, CSVCorpus): data_directory = settings.PP_NORWAY_NEW_DATA es_index = getattr(settings, 'PP_NORWAY_NEW_INDEX', 'parliament-norway-new') image = 'norway.JPG' - language = 'norwegian' + languages = ['no'] description_page = 'norway-new.md' document_context = document_context() diff --git a/backend/corpora/parliament/norway.py b/backend/corpora/parliament/norway.py index be04e2071..224784d26 100644 --- a/backend/corpora/parliament/norway.py +++ b/backend/corpora/parliament/norway.py @@ -27,7 +27,7 @@ class ParliamentNorway(Parliament, CSVCorpus): data_directory = settings.PP_NORWAY_DATA es_index = getattr(settings, 'PP_NORWAY_INDEX','parliament-norway') image = 'norway.JPG' - language = 'norwegian' + languages = ['no'] description_page = 'norway.md' document_context = document_context( context_fields=['book_id'], diff --git a/backend/corpora/parliament/parliament.py b/backend/corpora/parliament/parliament.py index 275c01645..6a979752c 100644 --- a/backend/corpora/parliament/parliament.py +++ b/backend/corpora/parliament/parliament.py @@ -34,12 +34,11 @@ class Parliament(Corpus): image = 'parliament.jpeg' data_directory = 'bogus' - language = 'english' category = 'parliament' @property def es_settings(self): - return es_settings(self.language, stopword_analyzer=True, stemming_analyzer=True) + return es_settings(self.languages[0], stopword_analyzer=True, stemming_analyzer=True) # overwrite below in child class if you need to extract the (converted) transcription diff --git a/backend/corpora/parliament/sweden-old.py b/backend/corpora/parliament/sweden-old.py index b065d9068..5c34d9a8f 100644 --- a/backend/corpora/parliament/sweden-old.py +++ b/backend/corpora/parliament/sweden-old.py @@ -48,7 +48,7 @@ def sources(self, start, end): yield csv_file, {} - language = 'swedish' + languages = ['sv'] description_page = 'sweden-old.md' image = 'sweden-old.jpg' diff --git a/backend/corpora/parliament/sweden.py b/backend/corpora/parliament/sweden.py index 2269db149..2be223160 100644 --- a/backend/corpora/parliament/sweden.py +++ b/backend/corpora/parliament/sweden.py @@ -52,7 +52,7 @@ def sources(self, start, end): yield csv_file, {} - language = 'swedish' + languages = ['sv'] description_page = 'sweden.md' image = 'sweden.jpg' diff --git a/backend/corpora/parliament/uk.py b/backend/corpora/parliament/uk.py index 66bc7963a..4d781abfc 100644 --- a/backend/corpora/parliament/uk.py +++ b/backend/corpora/parliament/uk.py @@ -40,7 +40,7 @@ class ParliamentUK(Parliament, CSVCorpus): es_index = getattr(settings, 'PP_UK_INDEX', 'parliament-uk') image = 'uk.jpeg' word_model_path = getattr(settings, 'PP_UK_WM', None) - language = 'english' + languages = ['en'] description_page = 'uk.md' field_entry = 'speech_id' document_context = document_context() diff --git a/backend/corpora/periodicals/periodicals.py b/backend/corpora/periodicals/periodicals.py index 37460fbe8..d946e372f 100644 --- a/backend/corpora/periodicals/periodicals.py +++ b/backend/corpora/periodicals/periodicals.py @@ -33,7 +33,7 @@ class Periodicals(XMLCorpus): image = 'Fleet_Street.jpg' scan_image_type = getattr(settings, 'PERIODICALS_SCAN_IMAGE_TYPE', 'image/jpeg') description_page = '19thCenturyUKPeriodicals.md' - language = 'english' + languages = ['en'] category = 'periodical' tag_toplevel = 'articles' diff --git a/backend/corpora/rechtspraak/rechtspraak.py b/backend/corpora/rechtspraak/rechtspraak.py index 35a4878ca..d97db5989 100644 --- a/backend/corpora/rechtspraak/rechtspraak.py +++ b/backend/corpora/rechtspraak/rechtspraak.py @@ -37,7 +37,7 @@ class Rechtspraak(XMLCorpus): es_index = getattr(settings, 'RECHTSPRAAK_ES_INDEX', 'rechtspraak') image = 'rechtszaal.jpeg' toplevel_zip_file = 'OpenDataUitspraken.zip' - language = 'dutch' + languages = ['nl'] category = 'ruling' tag_toplevel = 'open-rechtspraak' diff --git a/backend/corpora/times/times.py b/backend/corpora/times/times.py index f3ff50322..44ff5ce28 100644 --- a/backend/corpora/times/times.py +++ b/backend/corpora/times/times.py @@ -32,7 +32,7 @@ class Times(XMLCorpus): image = 'times.jpg' scan_image_type = getattr(settings, 'TIMES_SCAN_IMAGE_TYPE', 'image/png') description_page = 'times.md' - language = 'english' + languages = ['en'] category = 'newspaper' tag_toplevel = 'issue' diff --git a/backend/corpora/troonredes/troonredes.py b/backend/corpora/troonredes/troonredes.py index c33c24e7a..d00ec238e 100644 --- a/backend/corpora/troonredes/troonredes.py +++ b/backend/corpora/troonredes/troonredes.py @@ -35,7 +35,7 @@ class Troonredes(XMLCorpus): es_index = getattr(settings, 'TROONREDES_ES_INDEX', 'troonredes') image = 'troon.jpg' word_model_path = getattr(settings, 'TROONREDES_WM', None) - language = 'dutch' + languages = ['nl'] category = 'oration' tag_toplevel = 'doc' From 9ea225649c8a160617e113403e0654729dd8426f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 16:43:04 +0200 Subject: [PATCH 013/262] show metadata in corpus selector --- .../corpus-selector/corpus-selector.component.html | 4 ++-- .../corpus-selector/corpus-selector.component.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html index 7d2613465..72b51f8eb 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -27,8 +27,8 @@

-

Language: Dutch

-

Type: Parliamentary debates

+

Language: {{languages}}

+

Type: {{corpus.category}}

Period: {{minYear}}-{{maxYear}}

diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts index 17524d51f..8aac8e160 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts @@ -3,6 +3,7 @@ import { Corpus } from '../../models'; import { DialogService } from '../../services'; import { Router } from '@angular/router'; import { faInfoCircle, faSearch } from '@fortawesome/free-solid-svg-icons'; +import * as _ from 'lodash'; @Component({ selector: 'ia-corpus-selector', @@ -25,6 +26,10 @@ export class CorpusSelectorComponent implements OnInit { return this.corpus.maxDate.getFullYear(); } + get languages() { + return this.corpus.languages.join(', '); + } + ngOnInit(): void { } From d5a70fda3f336eb2b233b2ab6e856d394e225d3a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 17:05:48 +0200 Subject: [PATCH 014/262] add corpus filter component --- frontend/src/app/app.module.ts | 2 + .../corpus-filter.component.html | 30 +++++++++++++ .../corpus-filter.component.scss | 0 .../corpus-filter.component.spec.ts | 23 ++++++++++ .../corpus-filter/corpus-filter.component.ts | 44 +++++++++++++++++++ .../corpus-selection.component.html | 40 +++-------------- .../corpus-selection.component.ts | 3 -- 7 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html create mode 100644 frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss create mode 100644 frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts create mode 100644 frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6123e8a38..4847864a2 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -89,6 +89,7 @@ import { JoyplotComponent } from './visualization/ngram/joyplot/joyplot.componen import { VerifyEmailComponent } from './login/verify-email/verify-email.component'; import { DocumentPageComponent } from './document-page/document-page.component'; import { CorpusSelectorComponent } from './corpus-selection/corpus-selector/corpus-selector.component'; +import { CorpusFilterComponent } from './corpus-selection/corpus-filter/corpus-filter.component'; export const appRoutes: Routes = [ @@ -170,6 +171,7 @@ export const declarations: any[] = [ BalloonDirective, BarchartOptionsComponent, BooleanFilterComponent, + CorpusFilterComponent, CorpusHeaderComponent, CorpusSelectionComponent, CorpusSelectorComponent, diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html new file mode 100644 index 000000000..345408606 --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html @@ -0,0 +1,30 @@ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+ - +
+
+ +
+
+
+
+
diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts new file mode 100644 index 000000000..95160793b --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { CorpusFilterComponent } from './corpus-filter.component'; +import { commonTestBed } from 'src/app/common-test-bed'; + +describe('CorpusFilterComponent', () => { + let component: CorpusFilterComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + commonTestBed().testingModule.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CorpusFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts new file mode 100644 index 000000000..19c6438cc --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnInit, Output } from '@angular/core'; +import { Corpus } from '../../models'; +import { Subject } from 'rxjs'; +import * as _ from 'lodash'; + +@Component({ + selector: 'ia-corpus-filter', + templateUrl: './corpus-filter.component.html', + styleUrls: ['./corpus-filter.component.scss'] +}) +export class CorpusFilterComponent implements OnInit { + @Input() corpora: Corpus[]; + @Output() filtered = new Subject(); + + maxDate = new Date(Date.now()); + + constructor() { } + + get minDate(): Date { + if (this.corpora) { + const dates = this.corpora.map(corpus => corpus.minDate); + return _.min(dates); + } + } + + get languages(): string[] { + return this.collectOptions('languages'); + } + + get categories(): string[] { + return this.collectOptions('category'); + } + + ngOnInit(): void { + } + + collectOptions(property): string[] { + return _.uniq(_.flatMap( + this.corpora || [], + property + ) as string[]).sort(); + } + +} diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.html b/frontend/src/app/corpus-selection/corpus-selection.component.html index d9d1aed85..0a3fcd0ac 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.html +++ b/frontend/src/app/corpus-selection/corpus-selection.component.html @@ -8,39 +8,13 @@


-
-
- -
- -
-
-
- -
- -
-
-
- -
- - -
-
-
-
- -
-
-
+
+ +
+ +
+ +
-
-
-
- -
-
-
diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.ts b/frontend/src/app/corpus-selection/corpus-selection.component.ts index 2122cea34..51b50b62e 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selection.component.ts @@ -11,9 +11,6 @@ export class CorpusSelectionComponent implements OnInit { @Input() public items: Corpus[]; - minDate = new Date('01/01/1800'); - maxDate = new Date(Date.now()); - constructor() { } ngOnInit() { From 027c9d0000edf1176cdf658daecdc6db1a95bd86 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 17:39:57 +0200 Subject: [PATCH 015/262] add documentation on corpus metadata --- documentation/How-to-add-a-new-corpus-to-Ianalyzer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/How-to-add-a-new-corpus-to-Ianalyzer.md b/documentation/How-to-add-a-new-corpus-to-Ianalyzer.md index f34e5ac7f..a06552ba7 100644 --- a/documentation/How-to-add-a-new-corpus-to-Ianalyzer.md +++ b/documentation/How-to-add-a-new-corpus-to-Ianalyzer.md @@ -14,6 +14,8 @@ The corpus class should define the following properties: - `es_index`: the name of the index in elasticsearch. - `image`: a path or url to the image used for the corpus in the interface. - `fields`: a list of `Field` objects. See [defining corpus fields](./Defining-corpus-fields.md). +- `languages`: a list of ISO 639 codes of the languages used in your corpus. Check the list in `backend/addcorpus/constants`: you may need to expand it. Corpus languages are intended as a way for users to select interesting datasets, so only include languages for which your corpus contains a meaningful amount of data. The list should go from most to least frequent. +- `category`: the type of data in the corpus. The list of options is in `backend/addcorpus/constants`. The following properties are optional: - `es_alias`: an alias for the index in elasticsearch. From 85ded46a1c08b69713d7c667da101f35b2085b1d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 26 Apr 2023 17:44:06 +0200 Subject: [PATCH 016/262] styling of corpus selector --- .../corpus-selector/corpus-selector.component.scss | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss index b04fbb6a3..84e75f9eb 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss @@ -1,9 +1,5 @@ @import "../../../_utilities"; -.card { - cursor: pointer; -} - .corpus-container { padding: 0; color: $text-primary-color; @@ -11,16 +7,13 @@ background-color: $primary; > .column { - // compensate for lack of padding in corpus-container - padding-top: 20px; - padding-bottom: 20px; + padding: 1rem; // slightly more generous padding } } .image-column { position: relative; - margin-right: 10px; overflow-y: hidden; } @@ -44,9 +37,6 @@ color: white; } -.card-footer { - border-top: 1px solid $contrast-primary-color; -} .corpus-action { border: 1px solid $contrast-primary-color; From e5496e5eeb3e3774eb5e728decaf2f13934f56cf Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 28 Apr 2023 12:16:14 +0200 Subject: [PATCH 017/262] logic for corpus filtering --- .../corpus-filter.component.spec.ts | 38 +++++++++++++++- .../corpus-filter/corpus-filter.component.ts | 43 ++++++++++++++++++- frontend/src/mock-data/corpus.ts | 12 +++--- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts index 95160793b..43added3b 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.spec.ts @@ -1,7 +1,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { CorpusFilterComponent } from './corpus-filter.component'; -import { commonTestBed } from 'src/app/common-test-bed'; +import { commonTestBed } from '../../common-test-bed'; +import { mockCorpus, mockCorpus2 } from '../../../mock-data/corpus'; +import { Corpus } from '../../models'; describe('CorpusFilterComponent', () => { let component: CorpusFilterComponent; @@ -14,10 +16,44 @@ describe('CorpusFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CorpusFilterComponent); component = fixture.componentInstance; + component.corpora = [mockCorpus, mockCorpus2]; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should filter corpora', async () => { + let result: Corpus[]; + component.filtered.subscribe(data => result = data); + + const filterResult = async (language: string, category: string, minDate: Date, maxDate: Date) => { + component.selectedLanguage.next(language); + component.selectedCategory.next(category); + component.selectedMinDate.next(minDate); + component.selectedMaxDate.next(maxDate); + await fixture.whenStable(); + return result; + }; + + expect(await filterResult('English', undefined, undefined, undefined)) + .toEqual([mockCorpus, mockCorpus2]); + expect(await filterResult('French', undefined, undefined, undefined)) + .toEqual([mockCorpus2]); + expect(await filterResult(undefined, undefined, undefined, undefined)) + .toEqual([mockCorpus, mockCorpus2]); + expect(await filterResult(undefined, 'Tests', undefined, undefined)) + .toEqual([mockCorpus]); + expect(await filterResult('French', 'Different tests', undefined, undefined)) + .toEqual([mockCorpus2]); + expect(await filterResult('French', 'Tests', undefined, undefined)) + .toEqual([]); + expect(await filterResult(undefined, undefined, new Date('1920-01-01'), undefined)) + .toEqual([mockCorpus2]); + expect(await filterResult(undefined, undefined, new Date('1820-01-01'), undefined)) + .toEqual([mockCorpus, mockCorpus2]); + expect(await filterResult(undefined, undefined, undefined, new Date('1830-01-01'))) + .toEqual([mockCorpus]); + }); }); diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts index 19c6438cc..3e08ede52 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit, Output } from '@angular/core'; import { Corpus } from '../../models'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject, combineLatest } from 'rxjs'; import * as _ from 'lodash'; @Component({ @@ -12,7 +12,10 @@ export class CorpusFilterComponent implements OnInit { @Input() corpora: Corpus[]; @Output() filtered = new Subject(); - maxDate = new Date(Date.now()); + selectedLanguage = new BehaviorSubject(undefined); + selectedCategory = new BehaviorSubject(undefined); + selectedMinDate = new BehaviorSubject(undefined); + selectedMaxDate = new BehaviorSubject(undefined); constructor() { } @@ -23,6 +26,10 @@ export class CorpusFilterComponent implements OnInit { } } + get maxDate(): Date { + return new Date(Date.now()); + } + get languages(): string[] { return this.collectOptions('languages'); } @@ -32,6 +39,12 @@ export class CorpusFilterComponent implements OnInit { } ngOnInit(): void { + combineLatest([ + this.selectedLanguage, + this.selectedCategory, + this.selectedMinDate, + this.selectedMaxDate + ]).subscribe(values => this.filterCorpora(...values)); } collectOptions(property): string[] { @@ -41,4 +54,30 @@ export class CorpusFilterComponent implements OnInit { ) as string[]).sort(); } + filterCorpora(language?: string, category?: string, minDate?: Date, maxDate?: Date): void { + if (this.corpora) { + const filter = this.corpusFilter(language, category, minDate, maxDate); + const filtered = this.corpora.filter(filter); + this.filtered.next(filtered); + } + } + + corpusFilter(language?: string, category?: string, minDate?: Date, maxDate?: Date): ((a: Corpus) => boolean) { + return (corpus) => { + if (language && !corpus.languages.includes(language)) { + return false; + } + if (category && corpus.category !== category) { + return false; + } + if (minDate && corpus.maxDate < minDate) { + return false; + } + if (maxDate && corpus.minDate > maxDate) { + return false; + } + return true; + }; + } + } diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index ac6dd372d..277b003be 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -63,8 +63,8 @@ export const mockCorpus: Corpus = { index: 'test1', title: 'Test corpus', description: 'This corpus is for mocking', - minDate: new Date(), - maxDate: new Date(), + minDate: new Date('1800-01-01'), + maxDate: new Date('1900-01-01'), image: 'test.jpg', scan_image_type: 'pdf', allow_image_download: false, @@ -80,15 +80,15 @@ export const mockCorpus2: Corpus = { index: 'test2', title: 'Test corpus 2', description: 'This corpus is for mocking', - minDate: new Date(), - maxDate: new Date(), + minDate: new Date('1850-01-01'), + maxDate: new Date('2000-01-01'), image: 'test.jpg', scan_image_type: 'pdf', allow_image_download: false, word_models_present: false, fields: [mockField2], - languages: ['English'], - category: 'Tests' + languages: ['English', 'French'], + category: 'Different tests' }; export class CorpusServiceMock { From ab554aeacd1f6e2dd97c17caef3f9304dba2a416 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 28 Apr 2023 12:45:01 +0200 Subject: [PATCH 018/262] bind controls to selection data --- .../corpus-filter/corpus-filter.component.html | 12 ++++++++---- .../corpus-filter/corpus-filter.component.ts | 10 +++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html index 345408606..7f3593c57 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html @@ -2,13 +2,15 @@
- +
- +
@@ -16,13 +18,15 @@
- +
-
- +
diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts index 3e08ede52..76bf2ce6b 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -17,6 +17,8 @@ export class CorpusFilterComponent implements OnInit { selectedMinDate = new BehaviorSubject(undefined); selectedMaxDate = new BehaviorSubject(undefined); + maxDate = new Date(Date.now()); + constructor() { } get minDate(): Date { @@ -26,9 +28,6 @@ export class CorpusFilterComponent implements OnInit { } } - get maxDate(): Date { - return new Date(Date.now()); - } get languages(): string[] { return this.collectOptions('languages'); @@ -48,10 +47,11 @@ export class CorpusFilterComponent implements OnInit { } collectOptions(property): string[] { - return _.uniq(_.flatMap( + const values = _.flatMap( this.corpora || [], property - ) as string[]).sort(); + ) as string[]; + return _.uniq(values).sort(); } filterCorpora(language?: string, category?: string, minDate?: Date, maxDate?: Date): void { From 0afe39735f57a1590c6109be87fd5b3110dd41b5 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 28 Apr 2023 12:49:04 +0200 Subject: [PATCH 019/262] show filtered corpora in overview --- .../corpus-selection/corpus-selection.component.html | 4 ++-- .../corpus-selection/corpus-selection.component.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.html b/frontend/src/app/corpus-selection/corpus-selection.component.html index 0a3fcd0ac..061f480b0 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.html +++ b/frontend/src/app/corpus-selection/corpus-selection.component.html @@ -9,10 +9,10 @@


- +
-
+
diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.ts b/frontend/src/app/corpus-selection/corpus-selection.component.ts index 51b50b62e..6bcdac5cb 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selection.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Corpus } from '../models/corpus'; +import * as _ from 'lodash'; @Component({ @@ -11,8 +12,18 @@ export class CorpusSelectionComponent implements OnInit { @Input() public items: Corpus[]; + filteredItems: Corpus[]; + constructor() { } + get displayItems(): Corpus[] { + if (_.isUndefined(this.filteredItems)) { + return this.items; + } else { + return this.filteredItems; + } + } + ngOnInit() { } } From 8f3c2e2baa31fe02a4f7d2d2b1e77de3ece50f38 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 28 Apr 2023 13:16:45 +0200 Subject: [PATCH 020/262] add reset button --- .../corpus-filter.component.html | 62 +++++++++++-------- .../corpus-filter/corpus-filter.component.ts | 23 +++++-- .../corpus-selection.component.html | 1 + 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html index 7f3593c57..2c97d01bf 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html @@ -1,34 +1,44 @@ -
-
- -
- + +
+
+ +
+ +
-
-
- -
- + +
+ +
-
-
- -
-
-
- -
-
- - -
-
- +
+ +
+
+
+ +
+
+ - +
+
+ +
+
+ +
diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts index 76bf2ce6b..ba75c93fd 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -2,6 +2,9 @@ import { Component, Input, OnInit, Output } from '@angular/core'; import { Corpus } from '../../models'; import { BehaviorSubject, Subject, combineLatest } from 'rxjs'; import * as _ from 'lodash'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs-compat'; @Component({ selector: 'ia-corpus-filter', @@ -17,8 +20,17 @@ export class CorpusFilterComponent implements OnInit { selectedMinDate = new BehaviorSubject(undefined); selectedMaxDate = new BehaviorSubject(undefined); + selection: [BehaviorSubject, BehaviorSubject, BehaviorSubject, BehaviorSubject] + = [this.selectedLanguage, this.selectedCategory, this.selectedMinDate, this.selectedMaxDate]; + + canReset: Observable = combineLatest(this.selection).pipe( + map(values => _.some(values, value => !_.isUndefined(value))) + ); + maxDate = new Date(Date.now()); + resetIcon = faTimes; + constructor() { } get minDate(): Date { @@ -38,12 +50,7 @@ export class CorpusFilterComponent implements OnInit { } ngOnInit(): void { - combineLatest([ - this.selectedLanguage, - this.selectedCategory, - this.selectedMinDate, - this.selectedMaxDate - ]).subscribe(values => this.filterCorpora(...values)); + combineLatest(this.selection).subscribe(values => this.filterCorpora(...values)); } collectOptions(property): string[] { @@ -80,4 +87,8 @@ export class CorpusFilterComponent implements OnInit { }; } + reset() { + this.selection.forEach(subject => subject.next(undefined)); + } + } diff --git a/frontend/src/app/corpus-selection/corpus-selection.component.html b/frontend/src/app/corpus-selection/corpus-selection.component.html index 061f480b0..cc90ff34a 100644 --- a/frontend/src/app/corpus-selection/corpus-selection.component.html +++ b/frontend/src/app/corpus-selection/corpus-selection.component.html @@ -12,6 +12,7 @@

+
From 8defa1e297b0fcc1938275e3f31c86a221e66a6d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 28 Apr 2023 13:25:17 +0200 Subject: [PATCH 021/262] use primary colour in datepicker --- .../corpus-filter/corpus-filter.component.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss index e69de29bb..a5a02a474 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.scss @@ -0,0 +1,8 @@ +@import "../../../_utilities"; + +::ng-deep .p-datepicker { + margin: 1px 0; + .p-highlight { + background-color: $primary !important; + }; +} From 0c97e56d6f887388d6f87f81efd58961b1cc29bc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 28 Apr 2023 13:27:41 +0200 Subject: [PATCH 022/262] get maxDate from corpora rather than Date.now() --- .../corpus-filter/corpus-filter.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts index ba75c93fd..c9fb4068a 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -27,8 +27,6 @@ export class CorpusFilterComponent implements OnInit { map(values => _.some(values, value => !_.isUndefined(value))) ); - maxDate = new Date(Date.now()); - resetIcon = faTimes; constructor() { } @@ -40,6 +38,13 @@ export class CorpusFilterComponent implements OnInit { } } + get maxDate(): Date { + if (this.corpora) { + const dates = this.corpora.map(corpus => corpus.maxDate); + return _.max(dates); + } + } + get languages(): string[] { return this.collectOptions('languages'); From ba4e090cb823f5e97f59c886c469a33d7f05971e Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 2 May 2023 15:17:37 +0200 Subject: [PATCH 023/262] scaffold dbnl corpus --- backend/corpora/dbnl/dbnl.py | 22 + backend/corpora/dbnl/images/dbnl-logo.jpeg | Bin 0 -> 9158 bytes .../dbnl/tests/data/maer005sing01_01.xml | 4404 +++++++++++++++++ .../dbnl/tests/test_dbnl_extraction.py | 28 + 4 files changed, 4454 insertions(+) create mode 100644 backend/corpora/dbnl/dbnl.py create mode 100644 backend/corpora/dbnl/images/dbnl-logo.jpeg create mode 100644 backend/corpora/dbnl/tests/data/maer005sing01_01.xml create mode 100644 backend/corpora/dbnl/tests/test_dbnl_extraction.py diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py new file mode 100644 index 000000000..6a1da0dc6 --- /dev/null +++ b/backend/corpora/dbnl/dbnl.py @@ -0,0 +1,22 @@ +from datetime import datetime +import os +from django.conf import settings +from addcorpus.corpus import XMLCorpus + +class DBNL(XMLCorpus): + title = 'DBNL' + description = 'Digitale Bibliotheek voor de Nederlandse letteren' + data_directory = settings.DBNL_DATA + min_date = datetime(year=1200, month=1, day=1) + max_date = datetime(year=2020, month=12, day=31) + es_index = getattr(settings, 'DBNL_ES_INDEX', 'dbnl') + image = 'dbnl-logo.jpeg' + + tag_toplevel = 'TEI.2' + tag_entry = 'text' + + def sources(self, start = None, end = None): + for filename in os.listdir(self.data_directory): + yield os.path.join(self.data_directory, filename), {} + + fields = [ ] diff --git a/backend/corpora/dbnl/images/dbnl-logo.jpeg b/backend/corpora/dbnl/images/dbnl-logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..36198d43ba46b192ba00109ca84a409d8299176a GIT binary patch literal 9158 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 literal 0 HcmV?d00001 diff --git a/backend/corpora/dbnl/tests/data/maer005sing01_01.xml b/backend/corpora/dbnl/tests/data/maer005sing01_01.xml new file mode 100644 index 000000000..fe91c7960 --- /dev/null +++ b/backend/corpora/dbnl/tests/data/maer005sing01_01.xml @@ -0,0 +1,4404 @@ + + + + + + + Het singende nachtegaeltje + Cornelis Maertsz. + + + + Dit bestand biedt, behoudens een aantal hierna te noemen ingrepen, een diplomatische weergave van Het singende nachtegaeltje van Cornelis Maertsz. in de eerste druk uit 1671. + + + p. 9: 4 → 5: ‘5 Wel aen, ghy schoone, en ghy nette ieught.’ + p. 28: david → David: ‘Heeft David in sijn tooren.’ + p. 41: Veriogen → Verioegen: ‘Verioegen, en sloegen' + p. 42: 4. → 3.: ‘3. Weest wellekom Vorsten van Iacobs geslacht.’ + p. 43: thiemael → thienmael: ‘g' Hebt thiemael, den Philisteen meer ontbloot.’ + p. 48: leveu → leven: En geeft niemant in't leven.’ + p. 62: vulcht → vlucht: ‘Soo dacht hy met een snelle vlucht.’ + p. 63: Hemles → Hemels: ‘Soo comt des Hemels Heer.’ + p. 67: Danneer → Wanneer: ‘Wanneer als hy op rijst in de kimmen.’ + p. 69: Buyttn → Buyten: ‘Buyten 't spoor, en buyten 't padt.’ + p. 69: 3 → 2.: ‘2. 't Geselschap dat u hier toe-lacht.’ + p. 72: in het origineel is een gedeelte van de tekst slecht leesbaar. De redactie heeft de tekst tussen vierkante haken aangevuld: ‘De [y]delhe[y]t der Rijckdommen.’ + p. 72: in het origineel is een gedeelte van de tekst slecht leesbaar. De redactie heeft de tekst tussen vierkante haken aangevuld: 'Stem: Doe ic lest wandeld[e] over de Helder.’ + p. 75: sonde → soude: ‘Dus heerlijck soude werden opgetoyt.’ + p. 86: bruyseu → bruysen: ‘Haer gollefjes doet bruysen.’ + p. 86: 5. → 4.: ‘4. Die van laveeren dwers, en langhs.’ + p. 94: aenschowen → aenschouwen: ‘3. Daer sulje aenschouwen.’ + p. 96: FN → EN: ‘EN roemt niet van u Landt, noch Stadt.’ + p. 97: 8. → 3.: ‘3. Den Noor-man hackt gestaegh in 't Wout.’ + p. 108: koop mans goedt → koop-mans goedt: ‘4. Dan sal al des koop-mans goedt.’ + p. 110: 3. → 7.: ‘7. Maer soud' ick nu singen uyt.’ + p. 118: lnsten → lusten: ‘5. Wie soud' lusten nu te schieten.’ + p. 121: in het origineel is een gedeelte van de tekst onleesbaar. In deze digitale editie is ‘[...]’ geplaatst: Wanneer hy flau[...]r en naer.’ + p. 123: verbldt → verblijdt: ‘'t Zy men is verblijdt.’ + p. 127: vlas → vals: ‘Geen vals geruchte smeet.’, 'En haet vals sweeren.’ + p. 127: nytsuyght → uytsuyght: ‘Sijn naesten niet uytsuyght.’ + p. 127: nytstorten → uytstorten: ‘4. Die sijn gemoedt voor Gode kan uytstorten.’ + p. 138: fruy → fury: ‘10. Den satan valt in fury aen.’ + p. 139: flaverny → slaverny: ‘Van des satans slaverny.’ + p. 141: voleck → vloeck: ‘Sonde, Satan, Wet en vloeck.’ + p. 144: Steeren → Sterren: ‘Bij de Sterren.’ + p. 148: wreeet → wreet: ‘Als hy verstont dit wreet bedrijf.’ + p. 148: Eu → En: ‘5. Eu stapte mee een rassche voet.’ + p. 151: wrecken → wercken: ‘Het aerdtsche lichaem, en sijn wercken.’ + p. 159: 4. → 5.: ‘5. Wel wieje dan zijt, en laet u hert.’ + p. 161: 4. → 3.: ‘3. Komt Ierusalems Vier-schaer.’ + p. 164: Ie → In: ‘In't eerst veel goedts in t' lest veel straf.’ + p. 165: deu → den: ‘2. In den Eersten quammer voort.’ + p. 165: duysterhendt → duysterheydt: ‘Inde duysterheydt.’ + p. 171: Vermeedert → Vermeerdert: ‘Vermeerdert alle daegh.’ + p. 187: Het foutieve paginanummer 107 is verbeterd in 187. + p. 192: Nn → Nu: ‘Nu noodige vruchten al rijp, en gans.’ + p. 194: Massouw → Nassouw: ‘Stem: Treurt edel huys Nassouw.’ + p. 196: gemeeen → gemeen: ‘Dat is dan in 't gemeen.’ + p. 196: in het origineel is een gedeelte van de tekst slecht leesbaar. De redactie heeft de tekst tussen vierkante haken aangevuld: 'D[u]s aengeprickelt, soude.’ + p. 222: minmermeer → nimmermeer: ‘En nimmermeer.’ + p. 223: bijjven → blijven: ‘Hoe kan met Godt ons Vni blijven.’ + p. 224: vreuhtbare → vreuchtbare: ‘Veldt-Sangh, Op 't vreuchtbare ghewas in Iulius 1654.’ + p. 230: ougeluck → ongeluck: ‘My een ongeluck aentreft.’ + p. 233: schnnent → schijnent: ‘Als dan alleen, wanneer door 't schijnent' licht.’ + p. 234: d'outucht → d'ontucht: ‘Wanneer d'ontucht haer swang're wangen lost.’ + p. 239: 1645 → 1654: ‘Wellekomst, Aen Iuffr. Geertruyde Maria Bailly. Gekomen tot Wervershoof den 5. September 1654.’ + p. 240: Vrouwet → Vrouwe,: ‘WElkom, welkom waerde Vrouwe.’ + p. 244: nn → nu: ‘Doe nu ons Vaderlandt.’ + p. 246: lnst → lust: ‘Ghy die aen lust, en weeldt.’ + + + + + + + + + + + + + A1v + + + + + + maer005sing01_01 + DBNL-TEI 1 + +

2012 dbnl

+
+
+ + DSOLmetadata:yes + + + + + exemplaar universiteitsbibliotheek Leiden, signatuur: 1197 H 21 + +

Cornelis Maertsz., Het singende nachtegaeltje. Michiel de Groot, Amsterdam 1671.

+

 

+
+
+ + +

Wijze van coderen: standaard

+

+
+
+ + + Nederlands + + + + + Het singende nachtegaeltje + Cornelis Maertsz. + + + + + + + + + + + + +

+

+

+
+ + + Het singende nachtegaeltje + Cornelis Maertsz. + + + + + + + + + + + + +

+

+

+
+ + + 2012-09-17 + + SW + + colofon toegevoegd + + +
+
+ + +

 

+

Het singende Nachtegaeltje

+

Quelende soetelijck, tot stichtelijck vermaeck voor de Christelijck Ieught.

+

Door.

+

Cornelis Maertsz. tot Wervers hoof.

+

't Amsterdam Voor Michiel de Groot, Boek-Verkooper op den Nieuwen Dijck, 1671.

+
+ + + + + +Op De vermakelijke en stightelijke Liedekens van Cornelis Maarts +SOo wort de schrand're Rey der vloeiende Poëten +Door u, o waerde Vriend! vervult, +Soo wort uw' Naam met Eer vergult, +En door de Lof-bazuin roem rughtigh uitgekreten, + +De Dight-kunst scheen wel eer in Amstel silte Plassen +Alleen te sitten op haer throon, +Maar ghy stelt in uw' Dight ten toon +Dat in ons Wervershoof nogh eed'ler vrughten wassen. +Want't baat niet dat men kan een yd'le Pen beswang'ren +Met wonderlijck Gedight, +Indienmen niet en stight, +Maer met een Heydensch rot vervult de Mond der Sang'ren. +Ghy soeckt de Af-breuk van het Rijck des Helschen lagers. + +Dies ghy een Heiligh Ooghwit raakt, +En onse Ieught sticht en vermaakt +Soo volght ghy 't saligh Spoor des Ioodschen Harpe-Slagers. +Treet voort dien Eerbaan in, en laat geen Aardsch gewemel +V hind'ren in soo eed'len Saak, +Soo streckt uw 'Lands-lien tot een Baak +En voert ons dart'le Ieughd al singende ten Hemel. +H. Vander Meer. +
+
+ + +Toe-eygeninge +
+Aen de singende Ieught. +SOete jonckheyt, groene spruyties, +Die uyt keelties, soet als fluyties, +Met een suyck'ren gallem singht, +Dat het al in vreught op springht: +Ionge Dochters, soete liefies, +Minnaers-veulties, herte diefies, +Die door Honich soet geluyt, +Treckt mijn hert ter ooren uyt, +En laet het in wellust semmen, +Ende lobb'ren in u stemmen, + +Ende baden in de klanck, +Van uw' ziel-suygende sanck +t' Wijl dat op u roode lipjes +Even als koralen klipjes, +'t Swacke hobbelende boot +Van mijn hert, in stucken stoot. +Aengename Iongelingen, +Afgeveerdight tot het singen, +Minnaers van dat soet geslacht, +Dat ick mee seer hooge acht. +Ghy wordt nu van my gebeden, +Om wat by my in te treden: +Koom dan binnen, want ick nu +Eensjes singen sal voor u. +
+
+ + +[O Soete Ieught! Die in u jeughde zijt] +
+Stemme: Periosta. +O Soete Ieught! Die in u jeghde zijt +Des werelts pronck: en yder 't herte steelt, +Als ghy met sangh, in uwe vreugde blijdt, +Door soeten galm, veel droeve smerten heelt, +Ghy jaegt my oock mijn leet ter kelen uyt, +Door het geschal, op Keel en Veel, en Fluyt. +2 Niet als ghy singht 't geen tegen deughden strijt, +En door ontucht de kuyssche ziele smet: +Ach! doet dat niet, schoon ghy in vreugden zijt, +Satan dees strick tot u vernielen set, +En 't is vergift voor yder eerbaer hert, +'t Welck knaegt de deugt tot sy onweerbaer wert + +3 Maer als ghy sticht al wie u singen hoort, +En deught, en vreugt, al met malkander mengt +En Godes lof seer soet doet bringen voor, +Of nutte leer, den een aen d' ander schenckt: +En offert soo u stem, en sijnen keel, +In uwe jeught, aen Godt tot sijnen deel. +4 Voor u o jeught, die na dees dingen poogt, +Dicht ick onlanghs seer menigh stichtlijck liet, +'t Welck is een werk daer uyt ghy singhen mooght +Wat goets en soets: in't welck ghy lichtlijck siet +Wat dat de reyne lust, en vreughde scheelt, +Van 't geen door-gaens de yd'le ieughde speelt. +5 Wel aen, ghy schoone, en ghy nette ieught, + +Ick schenck dit werck aen u uyt goeden wil, +'t Welck u niet toont een vuyl besmette vreught, +Maer't hout de lusten in haer woeden stil, +'t Wijl 't door vermaeck doet vry van smerten gaen: +Wel neem dan dit geschenck van herten aen, +  +Van u lieven, ende waertsten, +Die u mint, +  +Cornelis Maertsen. + +
+
+
+ + +
+De stemme der Wijsheyt. Proverb. 1. 20. +Stemme: O Karsnacht: +DE wijsheydt die is uytgevaren, +Om haer aen 't volck te openbaren, +En daerom roeptse over-luyt +In al de poorten van de steden, +En al waer vele menschen treden, +Aldus voor 't volck haer reden uyt: + +2 Hoe lang, o dwase, domme sinnen! +Wilt ghy de dwaesheydt dus beminnen, +En gaen op spotters wegen voort: +Sal noyt de tucht uw' sin op-scherpen? +Of sult ghy alle tijdt verwerpen, +Des Heeren wil, en wet, en woort? +3 Wild och uw' gangen omme-keeren, +En schikt u gangh nae mijn begeeren, +En neemt de tucht van herten aen: +En ick sal alsdan u verklaren +Des levens padt, en openbaren +Aen u, den weg die ghy moet gaen. +4 Maer als ick roep uyt alle krachten, +En niemandt op mijn stem wilt achten, +Noch luysteren na mijn geseght: +En streck mijn handt uyt haer te vaten, + +En niemandt hem wil leyden laten, +Noch wijcken van sijn boosen weg: +5 Soo sal ick dan, als haer comt drucken +Den noodt, in doodt, en ongelucken, +Oock lacchen in haer tegenspoen: +En helpen niet wanneerse klagen: +Maer spotten dan, wanneer de plagen, +Van schrick, en angst haer sterven doen. +6 Daerom dat sy de ware leere, +Aen haer gegeven van den Heere, +Niet hebben in haer doen betracht: +En daer-en-boven, t' allen tijden, +Al mijn straffen en kastijden, +Niet aengenomen, maer veracht. +7 Laet haer dan wederom ter degen, +De vruchten van haer boose wegen + +Op eten, totse worden sat: +Want onheyl, ramp, en ongelucken, +Die spruyten uyt al sulcke stucken, +En wassen, op 't Godloose pat. +
+
+
+ +Volmaekten zegen. Psal. 144:12. 15. +
+Stem: Ghy loderlijcke Sylvia. +O Groot geluck! Voor die 't geniet, +O! zegen rijcke stroomen, +Die in sijn huys, sijn Soonen siet +Op wassen, als de Boomen: +Dat is een saeck // daer door vermaeck, +En eere werde becoomen. +2. Geluckigh, die behalven dien, +Sijn dochters magh aenschouwen, + +(Daer in men 't beelde des deughts can sien) +Als steenen uyt-gehouwen, +Die kant, en net // den Meester set, +In kostlijcke gebouwen. +3. En die sijn Schuuren zijn vol graen, +Daer elck vindt sijn behagen: +Diens Schapen op de Weyden gaen, +En duysent iongen dragen: +Daer door 't getal // vermeeren sal, +En wassen alle dagen. +4. Die sijn Ossen even staegh +Gaen in de velden ploegen, +Dat sy haer Heere alle daegh +Veel vruchten door toevoegen: +Die soo bevint // hoe veel hy wint, +En schept daer in genoegen. + +5. die van geen schade, noch verlies, +Sijn leven comt te hooren, +Noch krijght geen tijdingh onder dies +Dat hy wat heeft verlooren: +Noch geenen tijdt // een moort-gekrijt +Komt klincken in sijn ooren. +6. Geluckigh is een Mensch sijn staet, +Die dit is toe-gevallen: +Maer Saligh, in den hooghsten graet, +Ia Saligh boven allen +Is hy, die Godt // self tot een lot, +En deel is toe-gevallen. +
+
+
+ +May Liet +
+Stemme: Edel Karsouw. + +DE soete Mey +Verciert het velt met bloemen, +En alderhande kruyt, +Seer veelderley +Dat niet en is te noemen, +Nu uyt daer aerde spruyt. +De lucht is vol g'luyt, +Vol sangh, door 't quinckeleeren +Van 't gediert // dat hene swiert +Op hare rasse veeren. +2. De tijdt voor heen, +De Lente, en de Winter, +Ons nu niet meerder bijt: +Maer yder een +Nu tegenwoordigh vindter +Al gants een ander tijdt + +Weest nu oock verblijt, +En wilt den Heere loven, +Want al't goet, dat ons ontmoet +Dat komt alleen van boven. +3. Al wat verdort +Sijn cieraet had verlooren +En 't leven was ontgaen, +Dat selve wordt +Nu wederom herbooren, +En komt uyt 't graf opstaen, +Kruyt, en Gras en Graen, +Steeckt nu het hooftjen buyten, +En in jeught, vol soete vreught, +Komt lieffelijck uyt-spruyten. +4. Lof zy u Heer, +Bestuurder der nature, + +Die aen ons staegh gedenckt: +En altijdt weer +Na 's Winters kout besuren +De soete Mey ons schenckt, +En den Somer brenght, +Die met sijn rijcke gaven, +Ons staegh doet // met overvloet +En alle noodruft laven. +5. Kroont weer dit Iaer +Met u goedtgunsticheden, +En sent u zegen neer, +Geeft oock daer naer, +Dat wy in rust en vreden +Besorgen uwe eer: +Ende geeft, o Heer; +Dat wy na u behagen. + +In 't gemoet, staegh vruchten goedt, +Tot uwer eere dragen. +
+
+
+ +Minne-spiegel. +
+Stem: Ick quam in een Boomgaerdetjen. +MEn siet de Min, en haren dwangh, +In Iacob in haer kracht, +Een dienst van veerthien jaren langh, +Wort by hem kort geacht, +Alleenelijck om dat hy tot sijn loone +Den minnelijcken Rachel wacht. +2. Als niemandt hem onthouden dorst +Ontrent het open velt, +Wt oorsaeck dat de koude vorst +Daer bits, en vinnich knelt, + +Brant hy van liefde, ende wordet nimmer +Ia sijn verhert gedult ontstelt. +3. Als oock de Son door heeten brant +De nacht uyt 't velt verjaeght, +En door sijn vlam doet spleten 't landt, +Hy 't altemael verdraght: +Noch ys, noch vyer en kond hem nauliks krenken +Door suyv're liefde tot die maeght. +4. Wie is hy, die niet loven sal +Dees Minnaer, en Vriendin: +Geluckigh zijn so boven al, +Die dus uyt reyne min +Te samen voegen, herten ende handen: +Ick wensch geen ander Huys-gesin. +
+
+
+ +Dina geschent +
+ +Het Singende + +Stemme: O boole domme jeught. +GHy Dochters jongh, en teer, +Die geern vermeyen gaet, +Komt siet eens hoe u eer, +Dan in perijckel staet, +En hoe dit soet vermaeck +Vermenghelt is met gal +Dees' saeck, dees' saeck, dees' saeck, +Ick u vertoonen sal. +3. Dina niet wel bedacht, +Vingh eens het reysen aen, +En lette op de dracht, +Daer mee de Meysjes gaen: +En om oock self daer by, +Wat meerder nieuws te doen, +Kleed sy, kleed sy, kleed sy + +Haer in een vreemt fatsoen. +3. Doe sy dus moy, en schoon, +De Landen ommegingh, +Soo saegh haer Hemors Soon, +Een dertel iongelingh, +Die met sijn ooge sweeft, +Ontrent haer moye dracht, +En heeft, en heeft, en heeft +Die schoone Maeght verkracht +4. Doe dit nu was geschiet, +Denck eens, wat ongemack, +Dat onheyl, en verdriet, +Vyt dese daet ontstack: +Daer wort veel bloot gestort, +En wie in Sichem woont, +Die wort, die wort, die wort, + +Niet van het sweert verschoont. +5. Twee Broeders seer verstoort, +Om dese vuyle schant, +Begingen dese moort, +Self met haer eygen handt. +Den ouden Iacob treurd, +En nam't geweldigh swaer, +En scheurd, en scheurd, en scheurd, +Het hert schier van malkaer. +6. En Dina mist haer eer, +Daer elck sijn roem op draeght, +En sy en word niet meer +Gereeckent voor een Maeght +Niet een woordt meer en staet, +Van haer, den Bybel deur: +Ick haet, dees daet, en laet +Voortaen oock na van heur. +
+
+
+ + + + + +Op een Swellingh aen mijn Aengesicht. +
+Stemme: Ach droom hoe quelt ghy mijn gedachten? +WAt is ieught, en't leflijck bloosen +Van haren verw? 't zin meerder niet als Roosen +Die wel schoon, staen ten toon, +Cierelijck te proncken, +Root als bloodt // wonder soet // ende doet +Het gesicht van yder daer op loncken: +Maer als 't onweer daer opblaest, +Dan is al haer schoonheydt haest, + +Geheel te niet, als ware het versoncken. +2. Even alsoo zijn de schoonheden: +Een moy aenschijn, het ciersel van de leden, +Dat nu staet // tot cieraet, +Als een Son te schijnen, +Kloec en hel // eenen swel // terstont fel, +Al de schoonheydt veerdigh doen verdwijnen: +'t Blomtien dat soo aerdigh tierd, +Is dan in der haest ontcierd, +Men siet het sonder schoonheyt treurigh quijnen. +3. Sulcken les kan ick heden lesen +In mijn aensicht, en in mijn eygen wesen: +Want als ick // my bekick, +'k Sie dan mijn wanghen, +Noch onlangh // glad en blanck, nu vol stanck, +Ende vol van buylen etter hangen: + +Soo dat als men my besiet, +Ick gelijck my selven niet: +Maer 't schijnt, ik heb een ander hooft ontfangen. +4. Druck dit in 't hert, ghy jonge lieden, +En leert hier door, de trots, en hoogh-moet moet vlieden +Draeght geen roem // op een bloem +Teerder als de Roosen, +V aenschijn // kan haest zijn // gelijck mijn, +Als een bloemtjen dat nu is bevroosen +'s Werelts schoon, en't jeughlijck moy, +Dat kan even als het hoy, +Sijn groen verliesen, in seer korte poosen. +Den 25. Iannarius 1671. +
+
+
+ +Davids Huwelijck met Abigael. +
+ +Stemme: Ick roep u Hemelsche Vader aen. +EEn goet verstant, een wijs beleyt, +Verciert ons 't gantsche leven: +En die sijn dingen met bescheyt, +In goede billickheyt, +Voorsichtigh wel beleyt, +Die wordt daer door verheven +2. En 't is een saeck die seker gaet, +Wt wrevelmoedigh wesen, +Dat daer uyt voor gevolgh ontstaet, +Dat yder een hem haet: +Het welcke is een quaet, +Dat elck behoort te vreesen. +3. Een groote straf, een swaer verdriet, +Heeft David in sijn tooren, + +den Nabal (die hem niet ontsiet, +dat hy een volck verstiet, +daer van hy dinst geniet) +Seer heftelijck gesworen. +4. Maer Abigail gingh hem te moet, +Om hem eerst aen te spreken, +En doe sy viel den Vorst te voet, +Veranderd' hem 't gemoet, +Soo datse david doet, +Sijn quaden aenslagh breken. +5. door haer soet en vriend'lijk gelaet, +En haer geschickte zeden: +En daer en boven door haer daet, +Soo maecktse dat het quaet +Van haer geslachte gaet: +En dit was hare reden. +
+
+ + +Stemme: Schoonste Nymphje in het wout. +ACh! gesalfde van den Heer, +Vol van eer, +Hoor u Maeght geduldigh spreken, +Nabal is een heyloos man, +Daerom dan, +Laet doch na om dat te wreken. +2. Nabal deed u dit verdriet, +Daer ick niet +Wiste van de gantsche sake: +Toornt om soo een sot niet meer +Vromen Heer, +God bewaer u voor dees wraek. +3. God is Richter over al, + +En hy sal +Selver Nabal daer om straffen: +Siet, hier brengh ick een geschenck, +Ende denck, +Dit u krijghs-volck te beschaffen. +4. Wreeckt doch lieve David niet +V verdriet, +Nabal is u roed ontwossen: +God die selver voor ugaet, +Sal uyt smaet, +En verdriet u eens verlossen. +5. Dit te wreken is Gods saeck, +Laet de wraeck, +Dan aen hem bevolen blijven: +Ende soo wanneer als dan +Een Tyran + +V soud soecken te verdrijven, +6. Sal u ziele vry van last, +Sterck en vast, +In Gods hoopken zijn gebonden +Maer u boose vyant sal +Over al +Zijn geslingert in het ronde. +7. Ende als de grooten Godt, +V stelt tot +Eenen Heer, en Vorst van allen, +Soo en sal't knagen noyt, +Datje oyt, +Yemant in u toorn deed' vallen. +8. En de Heer op 's Hemels troon, +Sal tot loon, +V sijn milde zegen schencken: + +En daer na soo sult ghy weer, +Vromen Heer, +Vwe Maeght in gunst gedencken. +
+
+ +Stem: Roosemont die lagh gedoocken. +DOe de Koningh dit aenhoorde, +Liet hy van de wrake of, +En na 't sluyten van haer woorde +Riep hy uyt tot haren lof: +d'Alderhoogste lof en eer, +Zij bewaert voor God den Heer: +2. Maer naest hem, zijt ghy gezegent, +O! ghy zegen-rijcke mont: +Waerje my hier niet bejegent, +'k Had u volck in 't hert gewont, + +En u mannelijck geslacht, +d' Een, met d' ander omgebracht. +3. Oock ontfingh hy haer geschencken, +Ende seyde: Treckt nu voort, +En wilt om geen onheyl dencken, +Want ick heb u stem verhoort: +Gaet na huys, en weest gerust +Want mijn toorn is uyt-geblust. +4. Doe de Son nu met sijn wagen +Thienmael had rondom gereen, +Heeft God self den dwaes geslagen, +Nabals hert word als een steen, +En het leven tornt hem af, +Ende Nabal zijght in't graf. +5. Dit klonck strack in alle ooren, +Nabals wreetheyt is ten ent, + +Yder een quam dit te vooren, +En 't word David oock bekent: +Doe hy nu dees saeck verstont, +Opend' hy aldus sijn mont: +6. Wie en soud u, Heer, niet prijsen? +Noch u geven lof, en danck? +Wie soud u geen eer bewijsen, +Al sijn leve dagen lanck? +Ghy hebt al mijn spt, en smaet, +Nu gewroocken metter daet. +7. Heere, gy deed my beletten, +Dat ick in mijn ongedult, +My niet in u plaets gingh setten +'t Welck geweest had sware schult: +Wat my soo een mensche doet +Ghy, O Heere! wreecken moet. + +8. Ghy neemt oock de wraeck in hande, +Ende wreeckt mijn swaer verdriet: +Nabal maeckt ghy heel te schande, +En ghy brenght hem gants te niet, +Ia verslaet hem in het stof. +V zy Heere, Eeuwigh Lof. +
+
+ +Stem: Laura sat laest aen een Beeck. +DAvid die nu had verstaen +Van des Nabals haestigh sterven, +Dachte strack van stonden aen: +Nu sal ick het best verwerven, +Van al Nabals rijcke erven, +Dat is sijnen wijsen Vrou: +Daer wil ick door mijn Booden + +Abigael laten nooden, +Tot mijn gade in de Trou. +2. Hier op geeft hy sijn bevel, +Aen de dienaers van de Koningh, +En sy reysen ras, en snel, +By de Vrou in hare wooningh, +En met een beleefde tooningh, +Seggen sy dit voor haer uyt: +Weest gegroet, ghy wijse Vrouwe: +David die versoeckt in trouwe, +V te hebben tot sijn Bruydt. +3. Hier op sijne op haer wangh, +Roode pleckjes voort-gebroocken, +Evenwel ten duurd niet langh +Of sy heeft haer mondt ontloocken, +En in vreughden uyt-gesproocken: + +Ick ben tot dees saeck bereyt. +Daer mee reysden sy te samen, +Tot dat sy by David quamen, +Daer hy hare komst verbeydt. +4. Daer wordt dese wijse Vrou, +Met de Vorst gepaert in dughden, +En het gantsche landt wort nou +Wacker, en sprong op van vreughde: +Yder een hem seer verheugde, +Om dat Abigail niet meer, +Sal een stuuren kop verdragen, +Maer een wijsen Vorst behagen, +Siet, dus seltsaem wreckt de Heer. +
+
+
+ + +Minnaers Compasjen. +
+Stem: Poliphemus aen de strande. +DOe de hoogen Godt op eerde +Eerst formeerde +'t Edel menschelijck geslacht, +Heeft hy Adam, en sijn Vrouwe, +In de trouwe. +Met sijn eygen handt gebracht. +2. Noch op heden, in de sinne, +Woont de minne +Daer door dat het wijf, en man, +d' Een aen d' ander geeft te smaken. + +Sulck vermaken +Als haer niemandt geven kan. +2. Dese lust, des menschen leven: +In geschreven +Doet hem soecken sonder rust, +Tot hy in 't getal der menschen +Na sijn wenschen, +Vint sijn lief, en herten lust. +4. Maer in't soecken, en bejagen +Moet we vragen +'t Oogemerck van ware deught, +Soo sal onse trouw ons geven +Al ons leven, +Reyne lust, en vaste vreught, +5. 't Is veel grooter schat van weerden +Als op eerden + +Al de rijckdom geeft aen ons, +Datmen in ons jonge ieughde +Reyne deughde +Beyde brenght op 't pluymen-dons. +
+
+
+ +Rey der Israelitische Vrouwen. 1. Samuel 18:6. +
+Stem: Als Bocksvoetje speelt &c. +ALs Saul, en David den vyant in 't velt +Verioegen, en sloegen +Haer met gewelt, +En quamen in 't Hof // soo worde haer lof, + +Door 't gantsche landt Cana seer hoogh vermelt. +2. De vrouwen in't Israelitische landt, +Die namen, te samen +Den vedel in d'handt, +En springende voort // men singende hoort +Aldus haer schateren door het landt. +3. Weest wellekom Vorsten van Iacobs geslacht +Ghy helden, in velden, +Seer dabber geacht; +Den vyandt besweeck // wanneer hy u keeck, +Noch meer wanneer hy beproefde u macht. +4. Wel laet ons nu Saul veel lof, en veel eer +Bewijsen, en prijsen +Die dapperen Heer, +Die selver op 't landt // met eygener handt, +In't Heydens geslacht sloegh duysent ter neer. + +5. Maer singt noch veel hooger, o vrouwen gedans +Wy moete, begroete, +En setten de krans +Op David sijn hooft: Een yder nu looft +Dees eenighst, dees Fenix, het puyck der Mans. +6. Hy heefter thien-duysent alleenlijck gedoot +En velde, die helde +Seer machtigh en groot: +V daden zijn meer // als Sauls, uw' Heer, +g' Hebt thiemael, den Philisteen meer ontbloot. +
+
+
+ +Salomons Gebedt. 2. Paral.1. +
+Stem: Roosemont. Waer ghy vliet. + +DEs Hemels licht verdween, +Doe het groote licht verscheen, +Dat te boven gaet, de vlammende Son, +En hem openbaerd aen Solomon: +Namentlijck doe God, quam van boven af, +En hem dit antwoordt gaf: +Salomon eyscht ghy +Nu vrymoedelijk van my, +Wat dat u lust, en wat het zy. +2. Salomon hier op seydt: +Ghy hebt die Barmhertigheydt +Aen David gedaen, dat ick sijnen Soon, +Na hem ben gevolght, of sijnen troon, +Tot een Hooft van 't volck: en ik ben noch ionck, +En 't volck dat ghy my schonck, +Is sulck een getal, + +Dat niemant tellen sal, +En desen wachten op my al. +3. Daerom ghy wijsen Heer, +Sendt op my een straeltjen neer +Van u Hemels licht: op dat ick nu voort, +Dit volck leyden magh, soo het behoort. +Doe antwoorde Godt: nadien dat het wit +Van 't gene ghy my bidt, +Niet is machtigh goedt, +Noch oock niet uw's vyandts bloedt, +Noch niet dat ghy langh leven moet. +4. Maer alleen dat verstant +In u herte zy geplant, +Dat ghy Iocobs, stam, leyden meught in dees, +Na mijn wil, en woort, in mijne vrees. +Soo geef ick u verstant, wijsheyt, overvloet, + +Rijckdom, schatten, en goet, +En oock lof, en eer, +Alsoo datmen sulcken Heer +Niet en sal vinden immermeer. +5. Hier mee de Heer verdween, +En voer na den Hemel heen. +Ende Salomon, kreegh oock van den Heer, +Wijsheydt, en verstant, rijckdom, en eer: +Ia sulck een verstant, datmen nergens niet +Sijnes gelijcken siet +Silver, ende Gout, +Ende kostlijck Ebben hout, +Kreegh hy oock mede menigh-fout. +6. Salomon hier in speelt +Aen ons allen een voorbeelt, +Dat hy het verstant, acht in sijn gemoet, + +Meerder weert te zijn, als 't aertsche goedt. +Daer is oock geen schat, ons soo nut als dit: +Want die dien schat, besit +Heeft een instrument, +Daer door dat hy tot hem went, +Edele schatten sonder ent. +
+
+
+ +Gierigheydt. +
+Stem: Van de drie Dortsche Maeghden. +EEn Mensch die hoord te weten, +Datmen niet leeft om t' eeten, +maer eet op dat men leeft: +Dit moetmen wel bemercken + +In alle onse wercken, +Wat oorsaeck wercken heeft. +2. Maer die dit niet wel weten, +En deerlijck is beseten +Met dwase gierigheyt, +Die werckt niet om te leven, +Maer heeft hem gants begeven +Te leven om arbeydt. +3. Hy slaet hem self met roeden, +En leeft dus in armoeden, +Soo lange tot hy sterft: +En geeft niemant in't leven, +Maer moet het alles geven, +Aen eenen die het erft. +4. Dus dunckt my kan ick mercken, +Dat hy is als een vercken, + +Dat, t' wijle dat het leeft +Ons niet met al doet geven: +Maer na zijn gnortigh leven +Ons vele leckers geeft. +5. Hy is oock als de Peerde, +Die ploegen gaen op eerde, +Met moeylijck ongenucht: +Maer van 't verdrietigh ploeghen, +Van 't sweeten, en van 't swoegen, +En treckse self geen vrucht. +6. Men siet oock in hem blijcken, +Een Esel sijng practijcken, +Die dickmael seer bedroft, +Met sware ongemacken, +Draeght kostelijcke packen, +Daer hy noyt self van proeft. + +7. My denckt hy doet oock mede +Gelijck een Hondt eens dede, +Die op een Hoy-hoop lagh, +Waer hy niet van woud' eten, +En heeft wel luyd' gekreten, +Wanneer hy yemandt sagh. +8. jammerlijcke menschen: +Die staegh om rijckdom wenschen, +En woelen sonder rust, +Om eens vermaeck te vatten, +Maer al des werelt schatten +Verzaken noyt haer lust. +
+
+
+ + +Minnaers Noort-ster. +
+Stem: Het daget in den Osten. +MY dunckt dat eenen veugel, +Ellendigh is geplaecht, +Die niet meer als een vleugel +Op sijn kleyn lijfjen draeght: +Die kan hem na behagen +Niet wegh dragen. +2. Soo is 't oock met een herre +Die op een wiegel loopt, +Want sulck een rijedt niet verre +Of is wel dra gesloopt, +Dan scheurt het een, en't ander +Van malkander. + +3. Hier op soo denck ick stracken, +Hier aen sien ick nou, +Het leet, en ongemacken, +Des menschen buyten trou: +Want hem ontmoeten alle +Ongevalle. +4. De man met sijne Vrouwe, +Die is een vleesch, en been, +En daerom buyten trouwe, +Is elck de helft van een: +Dus moet we dese deelen +t' Samen heelen. +5. En soecken na genoegen +Een ander weder-helft, +Om die ons toe te voegen, +En t' smelten aen ons self, + +Om dus twee halve saken +Een te maken. +6. Alleen staet dit te myen, +Dat sonder goedt bestuur, +Wy niet bestaen te vryen +Een ongelijck partuur: +Dat soud' ons onlust geven +Al ons leven. +7. Men siet het noyt gebeuren +Dat yemandt Laken sneedt, +Van twee verscheyden kleuren, +En nayd' het aen een kleedt: +Of die dit doen, zijn sotten, +Meet te spotten. +8. Of saeghje twee Hant-schoene, +d' een nieu, en d' ander oudt, + +d' Een geel, en d' ander groene, +Ick weet, ghy seggen soudt, +Dit past niet, d' een, en d' ander, +By malkander. +9. Kiest dan tot u vriendinne, +Een die u wel gelijckt, +Opdat u hert, en sinne, +Van haer niet af en wijckt, +Maer dat gy liefd' mooght dragen, +Al u dagen. +
+
+
+ +Geestelijck Snoep-mes Matth.3:7.8.9 +
+Stem: Florida, soo het wesen magh. + +DOe eertijdts Iohannes vernam +Dat menigh tot sijn Doopsel quam +Met Phariseus gebreecken, +En dat hy sagh, oock meenigh een +'t Welck was een Saduceen, +Begon hy dus te spreecken: +2. Ghy boose Adderen geslacht, +Wie heeft de bootschap u gebracht, +Om Godes toorn t' ontvluchten? +Indien ghy soeckt na Godts genaet, +Soo toont ons metter daet +Nu boetveerdige vruchten. +3. En seght niet: wy zijn soo een stam, +Diens wortel is den Abraham, +Dit magh geen reden strecken: +Want soo ons Godt sijn macht liet sien, + +Hy kond uyt soo een stien, +Een Abraham kindt verwecken. +4. Ick segh u volgens mijn bescheyt, +De Bijl is heden aen-geleyt, +En veerdigh zijn de handen. +Om uyt te roepen onverschoont +Wat Boom geen vreucht en toont, +Om die dan te verbranden. +5. Ick doope wel voor u gesicht, +Een doop, doe ons tot boet verplicht? +Maer die my na komt loopen, +Die is veel weerdiger als ick, +Die sal sijn volk gelick, +Met sijn geest, en vyer doopen. +6. Sijn wan heeft hy in d' hant geree, +Te suyveren sijn dersch daer mee: + +En sal met sijne handen +De Terw vergaren in de schuur, +Maer sal het kaf in't vuur, +In eeuwigheydt doen branden. +
+
+
+ +Dwase Hoovaerdigheydt. +
+Stemme: Prins Robbaert. +EEn Aexter was eens bloot en kael, +En had niet op sijn huyt, +Sijn bonte veeren altemael +Waren ghevallen uyt, +Dies klaeghde hy met groot hert-seer +De vogelen zijn leet + +Woud' schencken tot sijn kleet. +2. Een yder was hier willigh toe, +En hebben dat gedaen, +Op dat hy in sijne arremoe +Van koud' niet sou vergaen: +Dit alle vog'len 't samen doen, +Elck pluckt een veertjen uyt, +Soo dat men root, en geel, en groen, +Sagh blincken op sijn huyt. +3. Doe nu den Aexter hem bekeeck +Hy van hem self verschiet, +En seyd, wie is't, die my geleeck? +Ick vind soo eenen niet: +Dus komt, o Vogels! nu ten toon, +Ghy alle te gelick, +En siet of yemandt wel soo schoon + +En cierlijck pronckt als ick. +4. Daer quaem wel haest een grooten som, +Van vogels kleyn, en groot, +Elck nam sijn pluymtjen wederom, +Daer word' hy weder bloot, +En stont daer met sijn naeckten huyt, +Gants kael en sonder kleet. +Hy schreyde bey sijn oogen uyt, +Maer 't holp niet tot sijn leet. +5. Dit toont so aerdigh als het mach, +Een pronckaert sijn bedrijf, +Die naeckt, en kael, quam voor den dagh, +En had gants niet om 't lijf: +Maer op dat hy dus niet versmacht, +En door de koud' vergaet, +Soo neemt hy meenigh beest sijn vacht + +En maeckt hem een gewaet. +6. Als nu den pronckaert, dit gewaet, +Rondom sijn lijf bekijckt, +Soo denckt hy met een trots gelaet, +Wie is't, die my gelijckt: +Maer dat elck 't sijne na hem trock, +Dat hy hem heeft gedaen, +Daer soud' den geck tot yders jock, +Met bloote billen staen. +
+
+
+ +Hooghmoedt voor den Val. +
+Stemme: Prins Robbert. +EEn Aep die eenen vreemden lust +In sijn gedachten kreegh, + +Was in hem zelven niet gerust, +Te blijven hier om leegh, +Hy sagh de swier der Vog'len aen, +Tot boven in de Lucht, +En dachte, ick wil mede gaen, +En trecken op de vlucht, +2. Hy maeckte vleugelen van Was, +Aen elcke zyde een, +En daer mee was hy wel te pas, +Want daer op vloogh hy heen: +De grillen in sijn malle kruyn, +Die dreven hem om hoogh, +Geen spitse toorn, noch hooge duyn, +Daer hy niet op en vloogh. +3. Ten lesten door sijn dwase sucht +Die dagelijcks aenwon, + +Soo dacht hy met een snelle vlucht +Te vliegen op de Son: +Maer desen dwasen overlegh, +Die word' in't eynde vals, +De Son die smolt zijn vleug'len wegh, +Hy viel, en brack sijn hals. +4. Een die met ander lieden gelt +Hem meent te maken rijck, +Door dien dat hem den hooghmoet quelt, +Is oock dees Aep gelijck: +Hy met een op-geblasen moet, +t' Onvreden in sijn staet, +Gaet hoopen t' samen goedt op goedt, +En staegh hy hooger gaet, +5. Dan ist dat hy met nijdt aensiet, +De gene die regeert, + +En hoe een yder 't hoogh gebiedt +Met kromme knien eert, +Dies Fyselt hy hem selven op, +Tot hooge macht en eer, +Tot dat hy op den hooghsten top, +Sit proncken als een Heer. +6. Maer wanneer hy in hooge macht +De Son wil zijn gelijck, +En over al de swarte nacht +Wil jaghen uyt sijn rijck, +Alleen door glans van sijn cieraet, +Soo comt des Hemels Heer, +En smelt sijn trotsen, hoogen staet: +Daer Valt den Aep dan neer. +
+
+
+ + +Boere Philosophy. +
+Stemme: Princesse hier koom ick by nacht. +WIlje wercken op goe voet, +Dan soo moet +Ghy wel letten wat elck doet: +Ghy moet yder Beest bestuure +Na de drift, na di drift van sijn natuure. +2. Heb je eenen moedigh Paert, +Schatten waert, +Laet het volgen sijnen aert, +En laet het u selven dragen, +Of het moet of het moet gaen voor de wagen. +3. Hebj' een Os, soo stelt hem vroegh +Voor de ploegh, + +Want het is hem spuls genoegh: +Maer kan hy dat niet betrachten, +Weyt hem ver, weyt hem ver, en wilt hem slachten +4. Hebje Koeyen, geeft haer Gras, +Op haer pas, +Anders het noyt voordeel was: +Want sy moeten der van leven, +En dan oock, en dan oock haer Melleck geven +5. Hebje Verckens, geeft haer veel +Garsten meel, +Yder rijckelijck sijn deel: +Geeft haer wey van uwe Koeyen, +Want sy moet, want sy moeter vet van groeyen. +6. Maer indien ghy hebt een Kat, +Geeft het wat +Van het witte Koeye nat: + +Laet het woonen in u huysen, +Want het vanght, want het vangter vele Muysen. +7. En indien ghy hebt een Hoen, +Wilt het voen: +Wat sal 't arme diertjen doen? +Daer en valt niet op te seggen, +Want het doet, want het doet, ons Eyers leggen. +8. Ende hebje noch wat meer, +Als een Heer, +Houdt u Beestjes frays in eer: +Want men moet na reden leven, +Self een Beest, self een Beest het sijne geven. +
+
+
+ + +Wereldts ghewoel. +
+Stem: Te may als al de vogelen singen. +HEt is een wonder om aen te mercken, +Hoe vele, en hoe verscheyden wercken +De menschen wel aenvaten: +Een yder die voeght hem tot het sijn, +In hoog, en lage staten, +2. Wanneer als wy de landen doorloopen, +Wat siene wy al menschen met hoopen, +In vaerten, en op wegen, +Die sommige loopen met ons heen, +And're komen ons tegen. +3. Dat ick eens op de maen mochte klimmen, +Wanneer als hy op rijst in de kimmen, + +En gaen met hem om hoogen, +Hoe frap soud' ick dit wereldts gewoel +Aenschouwen met mijn oogen? +4. Maer holla neen, ick heb my versonnen, +Wanneer ick dus was rondom geronnen +Soud' ick niet ander seggen +Als, 'k heb een krimmelende Mieren-hoop +Vol van mieren sien leggen. +
+
+
+ +Bekeeringe. +
+Stem: Wel op, wel op, ick ga ter jacht: +KEerom, keerom, o wereldts Kint: +Die de sonden seer bemint: + +Ghy gaet dwalen +Buyten palen +Buyten 't spoor, en buyten 't padt, +Dat ingaet in des Hemels, Stadt: +2. 't Geselschap dat u hier toe-lacht, +Dat wordt in de Hel verwacht, +En 't vermaken, +Dat sy smaken, +Sal haer op-breecken seer suur, +Hier namaels in het Helsche vuur. +3. Wijckt dan af van den boosen hoop +En kiest ghy een ander loop, +Eer sy hooren +Godes cooren, +Die opvaren doen vergramt, +Als eenen vuur seer brandigh vlamt. + +4. Valt dan oock met een yder aen, +Om u sonden te verslaen, +Heel te mortel, +Dat den wortel +Wt u hert magh zijn geroyt, +En die vyanden heel verstroyt. +5. Die door dick, en door dun soo heen +Heeft een vuylen wegh gereen, +En tot boven +Is bestoven +Laet dien wegh alleen niet naer +Maer maeckt hem oock van smeten klaer. +6. Soo zijn door sonden meenigh smet, +Leelijck aen de ziel geset: +Wilje wesen +Reyn van desen, + +Veeght dan al de smetten af, +Die oyt de sond' u ziele gaf. +7. Docht t' wijlwe door dit sterck fenijn, +Selver sonder krachten zijn, +Moet je klagen, +Ende vragen, +Waer is sulcken wijsen geest, +Die my van dit gebreck geneest? +8. Dan sulje hooren eenen stem, +Tot u roepen, (dat van hem, +Die de smerten Vwer herten +Kan omkeeren in genucht) +Komt tot my al die droevigh sucht +
+
+
+ + +De [y]delhe[y]t der Rijckdommen. +
+Stem: Doe ic lest wandeld[e] over de Helder +DAer zijn geen saucen die soeter doen smaken +Als doet den honger, die 't alles maeckt soet: +Daer is geen suycker, die 't drinken doet maken +Soo soet, en liflijck, als dorste wel doet. +Oock kan om schatten, den machtigsten Vorst, +Niet koopen honger, noch soeten dorst +Want dese beyde zijn meerder in weert, +Als al de schatten hier op der eerdt. +2. Yemandt verleckert op soete lusten, +Schud vry sijn bedde van pluymen wel sacht, +Nochtans sal hy niet vrediger rusten, +Als eenen Boer, slapend' op 't stroo by nacht. + +Men kan om schatten, noch groote rijckdom, +Geen slaep becomen in eygendom, +Noch rusie koopen: ja 't is altemet, +Dat rijckdom selver den slaep belet. +3. En ook een mantel die rammelt van Goude +En blinckt gelijcken de mane nu vol, +Beschut niet meerder voor bijtende koude, +Als slechte kleeren, van Schapen haer wol. +En oock de huysen aen d' Hemelen hoogh, +Decken niet beter, noch schuylen droogh, +Als eenen stulpjen, of Boere Hoy-schuur, +Het hout is mermer, als steenen muur. +4. Ook smaeckt het drincken uyt gouden vaten +Niet soeter, als uyt eenen pot van aerdt: +Sy smake de min in hoogere staten +Niet soeter, dan offer een Huys-man paerd: + +En groote kassen, vol silver gestoud, +Zijn niet van nooden tot onderhoud. +Een mensch sal leven van 't gene hem voedt, +Maer noyt sijn dage van overvloedt. +5. Daerom soo zijn het verdwalende menschen +Die altijdt woelen om rijckdom met smert. +Ick wil betrachten, en meerder niet wenschen, +Als noodtdroft, ende genoegen in 't hert. +Want ick en brochte ter wereldt gans niet, +Niemant in't sterven behouter yet. +Derhalven niemandt die meerder en heeft, +Als dat alleenlijck, daer hy van leeft. +
+
+
+ +Klachte der Godloosen. +
+Stemme: Lest lagh ick onder eenen boom, + +WAnneer als Godt zijn strengh gericht +Ten jonghsten dage komt te houwen, +Soo sal sijn volck, elck als een licht, +Ia Son zijn, in haer glans 't aenschouwen: +Maer het boos geslacht, +Dat haer heeft veracht, +Sal sien, wat haer dan wordt toegebracht. +2. Als sy dan dus Godts kind'ren sien, +Sal d' eene tot den ander spreken: +Ach wee ons doch! Wel wie is dien +Die daer komt als een Son voort breken? +Wy en dachte noyt, +Dat die gene oyt, +Dus heerlijck soude werden opgetoyt. +3. Wy achten haer, elck voor een dwaes, +En deden niet als haer bespotten: + +En nu zijn sy Godts volck Helaes: +Maer wy zijn nu verdoemde sotten, +Sy genieten rust, +Eeuwigh soete lust, +Terwijl dat ons vuur noyt wort uyt geblust. +4. Nu siene wy het klaerlijck aen, +Dat het werkeerde wegen waren, +Op't welcke dat wy zijn gegaen, +En daer door, aldus zijn gevaren. +Wy hebben van 't quaedt, +Ons eertijdts versaedt, +Daer door zijn wy in een verdoemde staet. +5. Mochten wy weer van vooren aen, +Des werelts-dal eens gaen betreden, +Wy souden beter wegen gaen, +En nutter onse tijdt besteden: + +Maer 't is al geschiedt, +Klagen helpt nu niet, +Ons rest niet anders als eeuwigh verdriet. +Besluyt. +6. Wy nu, die Godt noch vrind'lijck noot, +En stelt sijn genaden-deur open, +Laet ons niet op den wegh ter doodt, +Maer op den wegh des levens loopen, +Ende houden Godt +Voor ons deel, en lot, +En schicken ons leven na sijn gebodt. +
+
+
+ +Cupido's Fabel bevind ick vals. +
+Stem: O schoone die my dus mart'liseert? + +EEn oude Fabel, van lange wijl, +Stelt ons Cupido voor, +Dat hy met sijne boogh, ende pijl, +Des Minnaers hert schiet door, +Waer uyt terstont de minne swelt, +En maeckt dat hy sijn liefd' op yemant stelt. +2. Men stelt hem met een fackel in d' handt, +Gevult met brandent licht, +Daer door hy terstont in 't ingewant +Een Minne vlamme sticht: +Sy schild'ren hem oock als een kindt, +Sonder gesicht, en beyde oogen blindt +3. Die dit versierde seer grootlijcks loogh, +En die 't gelooft is mal: +Een kindt, een fackel, een pijl, een boogh, +Die doen hier niet metal: + +De Minne vlam, die soete brandt, +Die comt ons toe, van heel een ander kant. +4. De minne die sijnen scherpen schicht +My tot de ziel schoot in, +Quaem aen gevlogen uyt het gesicht +Van haer, die ick bemin: +Haer soet, en minnelijcken oogh, +Was 't altemael, de pees, de pijl en boogh. +5. De fackel die my in mijne jeught +Het herte stack in brandt, +Dat was haer reyne en witte deught, +Haer zeden, en verstant: +En daer quam oock haer schoonheydt by, +Waer by Heleen' niet te gelijcken zy. +6. Maer Cupido die en wasser niet, +Hy heeft my noyt geplaeght, + +Maer die mijn hart heeft in haer gebiet, +Dat is een jonge maeght: +Ick swijgh wie 't is: maer segge so, +mijn liefste in nu selver Cupido. +
+
+
+ +Valsche Vrintschup. +
+Stemme: Ick min mijn Herder. +DE valsche vrinde, +Die ons beminde +Om dat ons sake +Haer konde vermake, +Die neme strack de vlucht, +Wanneer ons treffen, druck, en ongenucht. +2. My dunckt sy icone + +Seer fraey, en schoone, +Dronckaerts maniere, +Die sitten te viere +Soo langh men tapt goe dranck: +Maer schenckt men haer hef, sy gane haer gank. +3. Dit is oock even +En na het leven +Gelijck als doene +De bladeren groene, +Die proncken den Boom schoon +Somers, maer 's Winters ontvallen sijn kroon. +4. Oock als de swalen, +Die in de salen +Ons soet toe singen, +Als ons alle dingen +Toe lacchen met genucht, + +Maer 't Winters dan trecken sy op de vlucht. +5. Is yemandt druckigh, +Hy is geluckigh, +Soo hy heeft vrinden, +Die hem altijdt dienden: +Maer 't is rijckst die leeft, +Die gene vrienden van nooden en heeft. +
+
+
+ +Bruylofs-Liedt. +
+Stem: De May die comt ons by seer bly. +GEluck, ghy die de tijdt van strijdt +In 't droevigh vryen uyt-gestreden hebt, +En met malkaer treet in 't begin + +Van soo een staet daer in ghy lusten scheyt, +Daer in dat ziel, en hert, +T' Samen gesmolten werdt, +Door vlamme van liefde, en minne-brandt. +Geluckigh Iongh-paer, +Nu vindt u te gaer +Een soeten bandt. +2. Men vint geen meerder soet, noch goedt, +Dat in de wereldt yemandt soo behaeght, +Als dan een minnaers hert, nu werdt +In eygendom aen sijn geliede maeght. +O soete lieve min, +Wat hebje vreughden in? +O haven van ruste! O stille Ree +Daer den Bruydegom +Pluckt de maeghde-blom, + +In lust, en vree. +3. Daer maeckt de Bruydt terstont gesont, +Haer minnaers hart, dat sy verwondet heeft. +Soo wanneer sy uyt lust hem kust, +Is 't ofje hem een nieuwen leven geeft. +Wel saligh is de lust, +Die aldus wordt gheblust: +Waer liefde en trouwe te samen gaen, +Wordt de eerste wet +Van Godt in geset +Als dan gedaen. +4. Ghy meysjes, treedt wel dra haer na, +En plaeght u trouwe minnaers langer niet, +Wiens herte dat ghy steelt, maer heelt +Haer droeve smerten, en haer groot verdriet, +Dees Bruydt die gaet u voor, + +Treedt ghy oock in haer spoor, +En volget de gangen, die sy nu gaet. +Lust, en leckerheydt, +Is voor u bereydt +In d' Echte staet. +
+
+ + Lof sangh van de Vliet. +
+Stem: Voorby is 't Winters herde stoet. +HEf op, mijn sangh-Godin, een Liet, +En met een soet gheschater, +Singh eens ter eere van de Vliet, +En van haer suyver water. +2. Het is een Bron vol soet genucht, + +Sy speelt musijck met suysen, +Soo wanneer als de Somer lucht +Haer gollefjes doet bruysen. +3. Dit ruyschende, en soet geluyt, +Door 't roeren van haer stroomen, +Dat lock de Steedsche jeughde uyt, +Om daer mee by te komen. +4. Die van laveeren dwers, en langhs, +Met vlaggen op haer Schuytje, +Wt 't welck men hoort veel soet gesanghs +En 't spelen op het Fluytje +5. De Knolle-vletter uyt de streeck, +Hoort men hier mede singen: +'t Vergadert al op dese Beeck, +Dat vreughde kan aen bringen. +6. Den Huys-man in sijn melleck-schuyt, + +Voeght hem by de Besanen, +En singende met soet geluyt, +Ontreckt de Zee haer Swanen: +7. Die latende de bracke Zee +En hare dorre stranden, +Beproncken nu de Vliet oock mee, +En al die naeste landen +8. De Koeten houden in het Riet, +En duysent and're mede: +Schier al wat men ter wereldt siet, +Versamelt daer ter stede! +9. De grondt die wimmelter van Vis, +(Het schijne wel mirakels,) +Die met vermaeck te vangen is, +Met Elgers, of met Schakels. +10. Of sooje maer een Fuyckjen set, + +Ter zijden voor een Slootje, +Slaept vry gerustelijck op u bedt, +Des morgens hebje 't sootje. +11. Des morgens legger by malkaer, +De Korpers, Baers, en Snoecken: +Men vanght oock licht een zood te gaer, +Met Angels, en met Hoecken. +12. Wanneer des winters koude Vorst +Ons knelt op onse rugge, +Soo stremt hy op de Vliet een korst, +Een held're glasen brugge. +13. Wat isser dan al reeds, en ganghs, +De Peertjes, wack're Beesies, +Die draven dan de Vliet al langhs, +Met duysent andere Sleesies. +14. Gewis, het is een soet vermaeck, + +By dese Vliet te woonen: +Hy self heeft kennis van dees saeck, +Die u dit doet vertoonen. +
+
+
+ +Seven personaedjen. Een Huys-man singht. +
+Stemme: Wie wil hooren singen. +DAer is geen staet te noemen, +Al schijnt een Huys-man slecht, +Dat reden heeft te roemen +Van sulcken ouden recht: +Want Kain, en Abel beyde, + +Die gingen op de weyde +En volghden die manier, +Als nu yder Huys-man hier. +2. Abram dat was een Herder, +Den vromen Iacob mee: +En komen wy wat verder, +'t Is al te doen met Vee: +Iob vulde met Schapen, en Ossen, +De Velden, en de Bossen: +Saul een kroone droegh, +En dreef even wel den ploegh. +3. Wat was 't dat haer dus maeckte +In desen staet gerust? +'t Was om dat sy dus smaeckte +Een leven vol van lust, +En datse dus niet en wisten, + +Van lagen, noch van listen, +Maer hadden al het soet, +In een vollen overvloedt. +4. Daer hoeft gheen gelt verspilt, +Gaet maer een weynigh delven, +Soo hebje, watje wilt: +Wy plucken al uyt den grase, +De Boter, melck, en Kase. +Den Acker deelt ons by, +Koorn tot meel, en leck're Bry. +5. Men weet van geen slampampen, +Daer na de Brasser hijght, +Die hondert duysen rampen, +Tot loon voor 't brassen krijght. +Wij soecken geen ydele eere, + +Men acht geen zijden kleere, +Ons vrolijck herte lacht +Om des wereldts malle pracht. +6. Want 't zijn veel beter dingen +Daer op den Huys-man lonckt, +De Vogeltjes die singen, +En 't veldt seer cierlijck pronckt: +Hy siet 'er het water stroomen, +En vruchten aen de Boomen, +En menigh schoonen Geest, +Dan onspringht hem sijnen geest. +7. Maer soo ick alle dingen, +Sou' trecken in mijn sangh, +Die ons vermaeck aen-bringen, +Ick song ses dagen langh: +Ia lichtelijck den Sondagh mede, + +Welk ick niet geern, en dede +Dies segh ick voor het slot: +O! geluckigh Huys-mans lot. +
+
+
+ +Een Stee-man singht. +
+Stemme: Phylis komt u buygen. +WIlie u vermaken, +Met verscheyden saken? +Wilie eenen staet +Sien, soo soet als heunich raet? +Komt dan onse Stad genaken, +Ende wandelt op ons straet. +2. Alles wat de Velden + +Den Huys-man bestelden, +Sijnen melck, en Room, +Komen met een snellen stroom, +Alles wat men hoordet melden, +Komt daer, en 't is wellekoom. +3. Daer sulje aenschouwen, +Huysen, en Gebouwen, +So schoon dat het oogh +Stadigh steygert na om hoogh: +Ghy sult nau u oogh betrouwen, +Want 't is of het u bedroogh. +4. Daer sulie aenmercken, +Hondert Ambachts wercken, +Alderley Fatsoen, +Daer sy 't Huys gesin door voen: +En 't gemeene best verstercken, + +Want elck heeft haer werck van doen. +5. Wat men kan begeeren, +Eten, drincken, kleeren, +En meer onder goedt, +Dat een yder hebben moet, +Ende niemandt magh ontbeeren, +Vindt ghy daer in overvloet; +6. Daer zijn hooge wallen, +(Noodigh boven allen +Tot des wereldts nut, +Die bepland staen met geschut, +'t Welck des Vyandts overvallen, +In haer dullen fury stut +7. Maer soud' ick uyt-singen +Alle goede dingen, +Die een geestigh man, + +Van de Stadt wel seggen kan, +Ick soud' hals, en keel verwringhen, +'t Is dan best, ick scheyder van. +
+
+
+ +Een Zee-man singht. +
+Stem: Hoort toe matroosen al te saem. +EN roemt niet van u Landt, noch Stadt, +Noch van u kost'lijck Vee, +Want al de rijckdom, en de schat, +Die komt u uyt der Zee: +Ick noem met bescheydt +De Schip-vaert 't wereldts hansen, + +Daer sy haer goedt mee leyt, +Van dees, in gene Landen. +2. 't Rijck Indien ontsluyt sijn mijn, +En levert vele Gout: +En Spanjen schenckt ons soete wijn, +En Vranckrijck geeft ons Sout: +Van elders komt ons Glas, +En Koper, Tin, en Verwen, +En Pick, en Teer, en Vlas, +En Roggen, ende Terwen. +3. Den Noor-man hackt gestaegh in 't Wout +De Boomen onder voet, +Niet voor hem self: maer dat hy t' hout +Na Hollandt senden doet. +Denckt nu in u verstant, +Wat raedt om dese dingen, + +Van 't een in 't ander Landt, +En over zee te bringen, +4. Hier weet een Boots-man daetlijck raet, +En komt in dit geval, +Met Schepen daer hy het in laedt, +En steeckt weer van de wal, +Om 't grondeloose ruym +In haest te overvaren, +Terwijl hy peeckel-schuym +Brouwt, in de soute baren. +5. Als dan soo een geladen vloot +Komt in de Steden aen, +Al was de Neeringh daer in doodt, +Sy soud' uyt 't Graf op staen. +En yder een ontfonckt, +En vindt dan sijn begeeren. + +En 't gantsche Landt dat pronckt +Gelijck in Bruylofts kleeren. +6. Maer soo de Zee-vaert wat vervalt +Het rijcke Landt ver-ermt, +De Stadt verliest strack sijn gestalt, +En yder Borger kermt: +Maer als een volle vloot +Komt zeylen in de Haven, +Krijght elck weer in de schoot, +Duysent, en duysent gaven. +7. Maer indien dat ick in't geheel, +Des Zee-vaerts nut uyt-songh, +Ick hoefde wel een yseren keel, +En een metalen tongh: +Maer 't is de pijn niet weert, +Die in mijn hooft te bringen: + +Soo yemandt meer begeert, +Die magh self meerder singen. +
+
+
+ +Een Koop-man singht. +
+Stem: Ick hoor aen dees vogelen singen. +WIlje weten wat de Neeringh +Aen de Stadt, en Landen schenckt? +Wat de welvaert, en verkeeringh +In de rijcke plaetsen brenght? +Ick segh dat des Koopmans handt, +In de Stadt, en in het Landt, +Al de schat, en rijckdom plant. +2. Schoon den Huys-man in de velde, + +Tot verwonderingh van elck, +Hondert Koeyen t' samen telde, +Yder een Fonteyn van melck, +Soo hy daer van niet verkocht, +Was hy in een staet gebrocht, +Daer hy niet in leven mocht. +3. Schoon de Stad met volle Salen +Op-gepropt van alle dingh, +Trots, en heerlijck staet te pralen, +Soo is 't noch, wat sy ontfingh, +'t Zy van schatten, ende gelt, +'t Zy van sterckheydt, en gewelt, +'t Is door ons daer in bestelt. +4. Schoon een vloot van duysent Schepen, +Na de vaert, en wel-vaert tracht, +Om veel waren in de slepen, + +Soo geen koop-man haer bevracht, +Blijft de waer in't selve Landt, +Daer 't den Schipper eerstmael vant: +En de vloot vertreckt met schant. +5. Schoon een vloot oock quam gevaren, +Van een verre, rijcke kust, +En hier brachte duysent waren, +Tot een yders vreught, en lust, +Kochte niemandt van die last, +Het bleef leggen by de mast: +Denck eens hoe of dat wel past. +6. Maer nu, wat de vreemde Landen, +Ons ontsluyten uyt haer schoot, +Goudt, en Sout, en alderhande +Dingen, noodigh in de noot, +Koopt den koop-man hier by een, + +Stapelt het in onse Steen, +Ende maeckt het elck gemeen. +7. Alsoo wordt door 's koop-mans handen, +Al het kostelijcke goedt, +Hier gebracht in onse Landen, +In een vollen overvloet. +Maer hield' ick met singen aen, +'k Song my dat ick niet mocht gaen +Daerom laet ick 't singen staen. +
+
+
+ +Een Krijghs-man singt +
+Stem: Een Ruytertjen jongh van jaren +INdienje eens willet letten +Hoe 't in de Wereldt gaet, + +Wat stijle men dient te setten, +Tot steunsel van yeder staet, +Gy sullet bevinden, dat Burger, noch Boer, +Noch koopman noch zeman, kan dragen op schoer +Des werelts welstant: maer dat het moet doen +Den Krijghs-man, met Degen en Roer. +2. Den Huys-man seer rijck, en prachtigh +En wel versien met gheldt, +Die isser nochtans niet machtigh, +Te weeren het wreedt gewelt: +Als komen de stroopers, en rooven sijn goedt, +Verliest hy sijn rijckdom, en mede de moedt. +Maer door den krijghs-man wel veylig bewaert +Soo verkrijght hy weer overvloedt. +3. De Steden alwaer vergaren, +Schatten van Landt, en Zee, + +Wel soude haer oock bewaren, +Indien het geen Krijghs-man dee? +Wanneer der een Leger de muuren beklam, +Of schoonder een Backer, of wever aen quam, +Och laci! wat was 't? sy souden van vrees, +Wel worden kreupel, en lam. +4. Schoon Zeeman al meent te zeylen, +Na meenich vreemden Landt, +Seer lichtlijck kan het hem feylen, +Soo dat hy wordt aengerant, +Van Roovers, en schuymers, en stroopers op zee: +Maer voert hy krijghs-helden, en wapenen mee, +Soo strooyt hy de stroopers: en vaert al voort, +In rusten, ende in vree. +5. Den Koop-man, die sijne goeden, +Doet stuuren wijt en breedt, + +Laet zijne Waren behoeden, +Van Helden met stael bekleedt: +Dit siene de Roovers, en neme de vlucht, +Want yder die isser voor 't leven beducht: +Dus come de ware des koop-mans aen, +Tot yders vreught, en genucht. +6. Wy binden met stalen banden, +Den wrevel, en 't gewelt, +En brengen de Dwinge landen +Geknevelt al uyt het velt: +Wy winnen de machtige steden van 't Landt, +En strooyen het ciersel van onsen Vyandt, +En voeren het in triumphe mee, +En planten het in ons Landt +7. Maer soud' ick al de profijten +Van d' Oorlogh singen uyt, + +Ick soude veel tijdt verslijten +En 't singen en geeft geen buyt: +Ick soude soo lange wel singen aen een, +Dat 't Leger wel soude innemen drie Steen, +En was ick daer niet dat speet my seer, +Adieu dan: want ick treck weer heen. +
+
+
+ +Een Medecijn-meester singht. +
+Stem: Seght mijn schoon Godinne. +VRaeghje wie het meeste goedt +Aen het Landt, en Steden doet? +Sonder lang beraden +Segh ick, dat een Meesters handt, + +Aen de Steden, en het Landt, +Doet de-nutste daden. +2. Daer en wordt geen mensch verschoont, +Rijck, noch arm, noch waer hy woont, +In de Stadt, of dorpen, +Of hy wordt licht kranck, en swack, +En is pijn, en ongemack +Stadigh onderworpen. +3. Als haer dan een sieckt verheft, +Als haer pijn, en lijden treft, +Door de gantsche leden, +Wat baet dan des Huys-mans Vee? +Wat baet dan de grijse Zee? +Wat baet dan de Steden? +4. Dan sal al des koop-mans goedt, +En sijn rijcken overvloedt, + +Hem niet heylsam wesen: +Dan ist met den Krijghs-man uyt, +Spiets, noch Sabel, roof noch buyt, +Can hem niet genesen. +5. Maer de Heere heeft het kruyt, +Dat hier uyt der aerde spruyt, +Gemaeckt een geneester +Van veel lijden, en ellent, +En maeckt hare kracht bekent +Aen een schrander meester. +6. Die dan stelt sijn kuur te werck, +Daer voor dat hy menigh sterck, +En gesont doet maken, +De dus door des meesters handt, +Blijven in een goede stant, +Al des wereldts saken. + +7. Maer soud' ick nu singen uyt, +Wat door Salf, door Smeer, en kruyt, +Wy al voordeel krijgen, +Ick soud singen alsoo langh, +Dat ick self word' sieck en bangh, +Daerom wil ick swijgen. +
+
+
+ +Een Predicant singht. +
+Stem: O schoon Kariclea +EEn mensche van natuur, +Is wel bequaem om yder Landt, en Stadt, +Door neerstelijck bestuur, +Te doen vervullen, met rijckdom, en schat, + +Maer kan niet vaten in sijn gemoet, +Hoe men moet winnen het hooghste goet. +2. En wat comt hem te baet, +Dat hy al in 't besit verkregen heeft, +Rijckdom, en eer, en staet, +Die hier de wereldt aen haer beminders geeft, +Soo hy de wereldt voor d' Hemel kiest, +En dan ten lesten die beyde verliest? +3. Wat is 't, of yemandt zeylt +Tot aen het ende van den Oceaen? +En alle gronden peylt, +En weet de werelt geheel romdom te gaen? +Soo hy met enen oock niet verstaet, +Wat wegh ten Hemel al binnen gaet? +4. En of een Krijghs-man oock +Door hem de gantsche werelt beven doet, + +En blaest staegh vuur, en roock: +Indien hy selven in sijn verwoest gemoet, +Sijn eygen driften niet en bevecht, +Soo blijft hy dan noch een Satans knecht. +5. Dies is in Landt, noch Stadt, +Schohon datse zijn vervult met machtigh goedt, +Geen kostelijcker schat, +Als 't woordt dat Godt ons nu heden schencken, doet, +En dan den Hemel alhier gestiert, +Ons Landt daer mede op't hooghst verciert. +6. Het wijst ons 't hooghste goedt, +En oock den wegh, op welcke men daer gaet: +En oock hoe yder moet +Hem dragen in sijnen wereldlijcken staet: +En wat voor plichten in elck geval, +Hy aen de menschen, en Godt, doen sal. + +7. Wilt ghy van nu aen voort, +Den zegen op ons Landen trecken neer, +Soo draeght u na het woordt, +Dat van den Hemel afcomt, van Godt de Heer. +Dan wordt de wereldt zegen bereydt, +En voor u selven de saligheydt. +
+
+
+ +Nu zijn de Personadjen uyt, Dies singh ick hier op tot besluyt +
+Stem: Maximilianus de Bossouw. +WIe wel bemerckt de ordeningh +Van al der menschen wercken, + +Die sal een soet en seltsaem dingh, +In dat gesicht bemercken, +Te weten hoe den een, en d'aer +Te samen spannen met malkaer, +De wereldt te verstercken. +2. Niemandt die 't al uytvoeren can, +Dat wel gedaen moet wesen, +Nu vaten wy't te samen an, +En alsoo wordt door desen, +Het Landt met al sijn rijcke Steen: +Ia heel de Wereldt in 't gemeen, +Behouden in haer wesen. +3. Dus wordt de werelt seer bequaem, +Van hoogh-geleerde Mannen, +Geleken by soo een lichaem, +Wiens leden t'samen spannen, + +Om elck te doen soo veel het can, +Waer door dat oock het onheyl dan +Verjaeght wordt, en verbannen. +4. Het ooge licht de voeten voor +De voet die draeght de handen: +Het herte luystert door het oor, +De buyck leeft door de tanden, +En alsoo voort met al de rest. +Soo is 't oock in't gemeene best, +Van Steden, en van landen. +5. Een yder blijf in sijn gelit, +En doe sijn eygen saecken, +Soo elck dit doet, soo sal oock dit, +Des wereldts welstant maken: +En't Landt in sulck accoort gestelt, +Is niet te winnen door ghewelt: + +Want 't harnast alle saken. +
+
+
+ + + + + +Rou-Klacht: Over de doodt van den Admirael Tromp: Geschoten den thienden Augusti 1653. +
+Stem: Mijne Harp bekleet met rouwe. +DRaeght nu vry geen swarte rocken, +Als men eertijdts placht: +Want het Landt is self betrocken + +Met een swarte nacht: +Eenen Son, tot 's wereldts wonder +Van elck een beschout, +Doock onlanghs in 't westen onder, +En soomd' het met goudt +2. Eenen muur, in 't rond gelegen +Om ons Vaderlandt, +Is nu onder voet gheslegen, +Door des vyandts handt. +Eenen onversaeghden Krijger, +Waer voor Spanjen vloot, +Meer als voor een Leeuw, of Tyger, +Laci! die is doodt. +3.Tromp, die is nu doodt geschoten, +En ter neer gevelt, +Nu is 't edel bloedt vergoten, + +Van dien dapp'ren Heldt: +Yder Burger hoort men weenen, +Treurigh, en bedroeft: +'t Gantsche Landt dat is met eenen +'t Herte toe-geschroft: +4. 't Bloedt verstremt my in sijn ader +Door de konde schrick, +Om dat Hollandts Vrede-vader +Is een leefloos Lijck. +Wie sal nu weer uyt Kartouwen, +Swanger met het Kruydt, +Donder, ende Blixem spouwen, +Met een groot geluyt? +5. Wie soud' lusten nu te schieten: +Laet den kanon staen: +En laet uyt u oogen vlieten, + +Eenen Zee van traen. +Draeght nu vry geen swarte rocken, +Als men voortijdts placht: +Want het Landt is self betrocken, +Met een swarte nacht. +
+
+
+ +Lof sangh van een goede Vrouwe. +
+Stemme: Mijn alderliefste verheven. +JCk voel mijn geest gedreven, +En aen-geprickelt stijf, +Om een Lof-sangh te geven +Aen een Godtsaligh wijf, +En aen haer vrom bedrijf. + +Om dit dan te beginnen, +Met in-getogen sinnen +Is 't, dat ick aldus schrijf. +2. Een Vrou begaeft met deughde, +Is als een sonne-schijn, +Die 't al verlicht in vreughde, +Alwaer haer stralen zijn: +Sy is een medicijn, +Die met haer lieflijck wesen, +Haer liefste doet genesen, +Van druck, verdriet, en pijn. +3. Sy is een stille haven, +En ruste voor de jeught: +Sy doet haer weergaed laven +Met soete lust, en vreught, +Daer door hem 't hert verheught: + +Het doet sijn geest verstercken, +Wanneer hy gaet bemercken, +Haer wijsheydt, en haer deught. +4. Sy is gelijck een wingert, +Vol druyven door malkaer. +Als sy haer arme slingert +Om haren weder paer, +Wanneer hy flau[...]r en naer. +In jammer soud' versticken, +Soo doetse hem verquicken, +En maeckt een vrolijck paer. +5. Sy is een goude kroone, +Die op het hooft des mans, +Pronckt cierelijck, en schoone, +Meer als een Peerlen krans: +Haer deught is Hemels glans: + +Sy is een schat van minne, +En oock haer Huys-gesinne +Een onvervinb're schans +6. Sy is een Hemels zegen, +Een gave van de Heer +Maer ick singh noyt ter degen +Haer wel verdiende eer: +Want sy verdient veel meer. +Wel hem, die soo een Vrouwe +Gheschoncken wordt in trouwe, +Door gunste van den Heer. +
+
+
+ +De snelligheydt des tijdts. +
+Stem: Lestmael ging ick op eenen morgen. + +GElijck een Vogel die daer henen +Vlieght, met een wack'ren snellen vlucht: +Of als een mist, in haest verdwenen +Voor 't aenschijn van de blauwe lucht, +Soo is oock even +'t Loopen van den Tijdt, +En van ons leven, +'t Zy men is verblijdt, +Of droeven smerten lijdt. +2. De nacht die kringht den dagh van stede, +Soo doet den dagh de nacht oock mee, +De Winter, en de Somer mede +Die drijven oock malkaer van 't stee +En al vreughde +Van des wereldts soet: +De groene jeughde, + +En haer bly gemoedt, +Vergaen gelijck een vloedt. +3. Schoon yemant op den troon geklommen +De wereldt had in sijn bedwangh +Noch stut hy niet, door groote sommen +Van yseren volck, de tijdt haer gangh: +Ia self sy allen +Sonden door de tijdt +Haest neder-vallen; +Want het is een strijdt +Die't alles neder smijt. +4. Dit wordt terstont ons voor-ghedragen, +Soo haest men in den Bijbel leest, +Daer staet van de ses eerste dagen, +'t Is Avondt, en morgen geweest: +Om dat de saken + +Des wereldts, in 't kort +Een eynde maken, +Gelijck den dagh wordt +In duysternis ghestort. +5. Alleen wordt d' Avondt niet beschreven, +Op 't lest van den sevensten dagh, +Om dat ons af-beeldt het leven, +Dat noyt de tijdt af-snijden magh. +Bouwt dan u sinnen +Niet op ydelheydt: +Maer wilt beminnen +'t Geen om hooge leydt, +'t Welck blijft in eeuwigheydt. +
+
+
+ + +Ken-tekenen van een Kindt Gods. Psalm 15. ende 24. Iesai.33.3 +
+Stem: Ay schoonste Nimph? aensiet, &c. +WIe ist die vry, met ongeboeyde ziele, +Bevrijdt van alle kruys, +Om hoogh sal gaen, als op Elias wielen +Tot boven in Godts huys? +En hoe moet hy, en sijn gestalte wesen, +Die voor Godts toorn ('t welck is een gloet, +Die al de boose gants verdoet) +Niet hoeft te vreesen? +2. Het is die geen die ongewoon tot liegen, + +Geen vals geruchte smeet, +Die niet een mensch sal wetende bedriegen, +Of oorsaeck zijn tot leet: +Die t' aller tijdt de kind'ren Godts doet eeren, +Maer 't boose volck dat Godt versmaet +Is vyandt door een heyl'ge haet: + En haet vals sweeren. +3. Die om gewin, door vuyle boose stucken +Sijn naesten niet uytsuyght, +Noch die oock niet, om d' arme t' onder-drucken +De Rechten omme-buyght. +Die door geschenck, noch oock door boose leere, +De suyv're waerheydt niet verlaet, +Noch oock het goede om het quaedt, +Niet doet verkeeren. +4. Die sijn gemoedt voor Gode kan uytstorten, + +En toont een suyver hert: +Die door bedrogh, oock niet en sal verkorten +Sijn schulde, tot yemandts smert. +Die oock in tucht alsoo bedwinght sijn oogen, +Dat sy niet leyden tot het quaedt: +Die door des wereldts schijn-cieraert, +Niet werdt bedrogen. +5. Siet die is het, die daer sal gaen om hoogen +En eeuwigh aldaer zijn, +En alle heyl geniet voor Godes oogen, +En salige aenschijn: +De Hemel sal zijn stercke vestingh wesen, +Daer hy met zegen overstort, +In eeuwigheydt behouden wordt +Sonder te vreesen. +
+
+
+ + +De voorspoedt der Godloosen. Psalm 73:2. 18. +
+Stemme: Granida Princesse. +ICk was schier geweken +Van de rechte paden, +En verdwaelt door enckel ongedult, +En door nijdt ontsteken, +Doe ick sagh de quaden +Met rijckdom, in voorspoet opgevult: +Sy schricken noyt in doodts gevaer een reys, +Maer staen soo vast gelijck een sterck Paleys. +2. Geene ongelucken, + +Noch geen harde slagen, +Nu gemeen by 't menschelijck geslachte, +Komen haer oyt drucken. +Heeft dan Godt behagen +In haer quaedt, en in haer trotse pracht? +In wulpsche weeld' mest elck van haer hem vet, +'t Wijl hy Godts volck met lasteren besmet. +3. Als oock sulck een harer +Sijn lippen doet open, +Is 't of sijn stem van boven comt of, +Dan komter als water +Het volck aen geloopen, +En offert aen hem seer grooten lof, +En prijst seer hoog, sijn hooghmoet, en sijn trots, +En vloeckt met een de lieve kind'ren Godts. +4. Siet dat is ter degen + +'t Fatsoen af geschildert, +Ende haren aert gecontarfeyt: +Maer ick daer en tegen, +En ben noyt verwildert, +En heb na Godts Wet mijn koers geleydt, +Maer wordt geplaeght, alwaer ick my begeef: +Is 't dan vergeefs, dat ick onstraflijck leef? +5. Siet in sulcke reden +Woud' ick schier uytvaren, +En had oock by na haer seer genoemt: +Maer dan had ick heden +Godts volck die oyt waren, +En nu zijn behouden, heel verdomt. +Ick dachte oock: wel waer comt dit van daen? +En kond' dees saeck ten degen niet verstaen. +6. Tot dat ick gingh treden + +Op het alderleste, +In Godes Huys, en in sijn Heylighdom, +Daer vondt ick de reden, +Waerom sy soo meste +Sadt en vet: de reden is, daerom, +Godt heftse hoogh, eer heyse neder-stort, +Op dat haer val alsoo te grooter wordt. +
+
+
+ +De bekeerde Moordenaer, Luc. 23. +
+Stemme: De Herdertjes in de nacht. +EEn moordenaer die altoos // Goddeloos +Vermoorde, en verscheurde, gelijck een Beer + +Die nimmer niet hoord +Van Christus, noch sijn woordt, +Die noemde aen 't Kruyce, Iesus sijn Heer. +t' Wijl Petrus in de Zael +Hem loochende driemael, +Hem Iudas verrade: Hy selver met pijn +Beladen soo seere, +Riep: Waerom, Ach mijn Heere, +Verlaet ghy nu mijn? +2. t' Wijl yder in dit verdriet // hem verliet, +En vluchte eer hy was aen 't Kruyce gehecht, +Soo datter gants geen blijck +Was, van zijn Koninghrijck, +Sagh nochtans dees moorder sijn Konings recht +t' Wijl een Apostel hingh +En moorders loon ontfingh, + +Soo worde een moorder een Euangelist: +Die onlanghs seer sondight, +Nu Christi lof verkondight: +Wie had dat gegist? +3. Ia selver dees moordenaer // was voorwaer +Veel wijser als d' Apostels alle gelijck, +Sy vraeghden noch daer na +Sult ghy, o Heere dra +Aen Israel herstellen het koninghrijck? +Sy sagen Iesus aen, +Voor een, die soud' verslaen +De Roomsche Tyrannen, en storten haer neer. +Maer desen in't sterven +Die bidt des Hemels erven +Van des Hemels Heer. +4. Hoe konde dees mordenaer // also klaer + +Sien, door dees dicke wolcke, en nevelen heen, +Dat hy in dit ellent, +De Godtheydt Iesu kent, +Daer hy eenen leydts-man der moorders scheen: +Het is 't geloof geweest, +Gewrocht door Christi Gheest, +De welcke hem Iesus dus maeckte bekent: +Nu leeft hy hier boven, +Om sijn Heylandt te loven +Eeuwigh sonder endt. +
+
+
+ +Vyanden der Kinderen Godts. +
+Stemme: Voorby is's winters harde stoot. + +VOor swellen die besloten zijn +Sal meer een meester vreesen, +Dan of alleenelijck de pijn, +Comt buyten aen te wesen. +2. Op ondiep Water, voor een Klip +Die loert in het verborgen, +Soo draeght een Stuurman voor het Schip, +De aldergrooste sorgen. +3. En een voorsichtigh Capiteyn, +Is voor verborgen lagen +Veel meer bevreest, dan of alleyn, +Maer wancken harde slagen. +4. Alsoo is oock 't verdorven vlees +De quaetst van ons vyanden, +En darom oorsaeck, dat ons dees +Bedecktlijck komt aenranden. + +5. Sy houdt gedurigh scherpe wacht. +En laet geen tijdt ontsluypen, +Maer neemt op ons gestadigh acht, +Ons wacht te onder-kruypen +6. Den Satan heeft met haer verdrach, +En loopt staegh om de kanten, +En soeckt waer hy sijn Standaert magh, +Op wal, en vestingh planten. +7. Het vlees onthout haer by de poort, +En soeckt hem in te leyden, +En tracht aen dees, of d'ander oort +Hem ingangh te bereyden. +8. Somtijts soo vint sy inder nacht, +Gants sonder sorge slapen, +Die daer gesielt zijn op de wacht: +Dan neemt sy haren wapen. + +9. En roepter strack den satan by, +En seydt: komt treet u binnen, +Want dese stadt, voor u, en my, +Die is nu licht te winnen. +10. Den satan valt in fury aen +En meent die stadt te rooven +Maer als hy binnen is gegaen, +Soo komt den Heldt van boven. +11. Den Hoeder Isr'els in de lucht, +Met sijn Geweer in handen, +En drijft den satan op de vlucht, +Soo dat hy ruymt met schanden. +12. Wel looft u Godt, na rechten eys +Ghy alle sijne kind'ren, +Die soo den satan, en het vleys, +Haer moed-wil doet verhind'ren. +
+
+
+ + +Geestelijcke triumphe. +
+Stem: Hey hoe helder schijnt het maentje? +LOoft Godt alle ghy verloste +Van des satans slaverny: +Al uw' vyanden die moste. +V gaen laten los, en vry, +Doe quam // dat Lam, +Dat voor ons voldeed, +En Godts toorne voor ons leed: +Dat Lam // dat voor ons voldeed, +En steld Godt met ons te vreed. +2. Al des satans Helsche stricken, +Door ons sonden sterck in kracht, + +En de wet, die met verschricken, +Den sondaer voor 't oordeel bracht, +Zijn nu // voor u, +Ghy die Christi zijt, +Gants vernielt voor alle tijdt. +Voor u // ghy die Christi zijt, +Is geen schrick voor hel, of strijdt. +3. Dreyght de doodt u te verworgen, +Segh dan vry, O doodt! soo niet, +Eertijdts hebt ghy mijn Borge +Vast gehadt in u ghebiedt: +Maer hy // gingh vry, +Doe hy had betaelt, +En mijn schulden door gehaelt. +Maer ghy // doe hy had betaelt, +Hebt ghy niet als schand behaelt. + +4. Alle sijnse soo gevaren, +Sonde, Satan, Wet en vloeck, +Schoonse thienmael meerder waren, +Noch was Christus haer te kloeck: +Hy bracht // haer macht, +Gantschlijck onder voet, +Door sijn lijden, en sijn bloedt. +Haer macht // gantschlijck onder voet, +Maer ons schonck hy 't eeuwigh goedt. +5. Wat sal ick nu weder geven +Aen hem? schatten groot van som? +O neen! Ick wil self mijn leven +Schencken hem in eygendom: +Ick sal // het al, +Lijf, en Siele beyd, +Overgeven sijn beleyd. + +Ick sal // Lijf en Ziele beydt, +Schencken hem in eeuwigheydt. +
+
+
+ +Toe-vlucht der Geloovigen. +
+Stemme: Polyphemus aen de strande. +JCk weet Heere, ghy sijt seecker, +Eenen wreecker, +En een straffer van het quaedt: +Wilt ghy dan in toorn verbolgen, +My vervolgen, +Soo en weet ick geenen raedt. +2. Schoon ick snelder als de veug'len, +Op de vleug'len + +Van de gulde dageraet, +Vloogh aen, t'eynde van het water, +Wiens geklater +Staegh de wreede Rotsen slaet: +3. Noch soud ick my self bedriegen, +En in 't vliegen, +Van uw' handt daer zijn bekayt: +Want ter wereldt niet en repter +Daer den Scepter +Vwer handt niet overswayt. +Schoon ick onder de Zee-golven +My bedolve, +En ick daer in't wel-sandt kroop, +Om soo in die diepe kuylen +V t' ontschuylen, +'k Was daer mee al buyten hoop. + +5. Schoon ick onder in der Hellen, +Socht te stellen, +Eenen Schuyl-plaets voor u oogh, +Noch souden uw' ooge stralen +Op my dalen +In der Hellen, van om hoogh. +6. Schoon ick mede soude derren +Bij de Sterren +Klimmen op een gouden stoel, +My soud' al het selve schorten, +Ghy soud' storten +My in d' onder-aerdtsche poel. +7. Schoon ick in de harde klippen +Konde slippen, +En haer zijn tot ingewant, +Daer my Son, noch maen, noch Winden, + +Konde vinden, +Soud' my vinden uwe handt. +8. Al het vluchten voor Godts tooren +Is verlooren, +Schoon ick was in 't stercktste slot +Datter is in 't Aertsch gewemel, +Onder d' Hemel, +Ick was daer noch naeckt voor Godt. +9. Sal ick dan niet angstigh beven +Ende geven +My de wan hoop tot een roof? +O neen! Ick wil noch al hopen, +En doen open +'t Scherp-siend' ooge van 't geloof. +10. Schuylende in Christi wonden, +Daer mijn sonden + +Voor Godts oordeel zijn bedeckt, +Ende wil mijn naeckte leden +Doen bekleeden +Met sijn deughden onbevleckt. +11. Laet dan vry Gods toorne blaken, +En doen kraken +Beyde Hemel, ende Aerd, +Evenwel sal ick dan wesen +Sonder vreesen, +En in Iesum wel bewaert. +12. Iacob dede my dit leeren, +Die met kleeren +Van sijn Broeder hem omhingh, +Ende alsoo quam voor Isack, +Aldaer hy strack +'s Vaders zegen door ontfingh. +
+
+
+ + +Simsons Raedtsel, Iudic. 14:14. +
+Stemme: O saligh heyligh Bethlehem. +DAer quam van eenen eter spijs, +En van een stercken soetigheden. +Nu is de vraghe, op wat wijs +Moet men dit Raedtsel recht ontleden? +Antwoort. +2. By Timnath was een wreeden Leeuw +Een Pest, en plage voor de Velden, +Die staegh verweckt een moort-geschreeuw, +En 't Landt in een verbaestheydt stelde. +3. Een yder mensch en dorste schier +Dat gantsche Landt niet eens genaken: + +Want dat vervaerlijck Monster-dier +Verscheurd haer in sijn wreede kaken. +4. Maer Simson, welckets gantsche lijf +Met stalen zenuwen was door-regen, +Als hy verstont dit wreet bedrijf, +Voeld' hy sijn geest in hem bewegen. + 5. Eu stapte mee een rassche voet +Na Timnaths wout, dat Beest te stooren, +Daer hem dien Leeu terstont ontmoet, +Die strack vergramt in heete tooren. +6. En braeckt den Donder uyt sijn keel, +En schoot den Blixem uyt sijn oogen, +Niet anders, dan of hy geheel +De Helle hadde uyt-gespogen. +7. En quam met een geresen pruyck: +Met krullende bebloede locken, + +En meend' oock Simson in sijn buyck +Als and're lieden op te slocken. +8. Maer Simsom hief sijn handt eens op, +Die hy eerst eens rondomme swayden, +En trompt den Leeuw voor sijnen kop, +Dat hem sijn beyde oogen draeyden. +9. Daer stort dien Leeu, dat grousaem dier, +Dat soo op sijn krachten steunde, +En gaf een Brul, met sulck getier, +Dat al het Landt rondomme dreunde. +10. Simson met een metalen vuyst, +Die scheurd van een, de kop, en lenden: +Daer lagh dat Beest, met vuyl begruyst. +En Simson gaet hem elders wenden. +11. Maer als Simson daer na eens gaet +Voor by dees vuyle doode prye, + +Soo vondt hy daer in Honingh-raedt, +Daer in vergadert door de Byen. +12. Siet hier uyt neem ghy klaerlijck af, +Soo ghy met vlijt maer wilt aenmercken, +Hoe dat den eter spijse gaf, +En soetigheydt quaem van den stercken. +
+
+
+ +De Mensche ellendigher als de Beeste. +
+Stem: Hoe legh ick hier in dees ellende. +O Soete jeught weest niet hooveerdigh, +Roemt niet op staet, of ed'len geest, +Want, schoon al schijnt een mensch seer weerdig, + +Noch is hy minder als een Beest: +Soo wanneer wy te recht bemercken, +Het aerdtsche lichaem, en sijn wercken. +2. De jonge Kalveren, en Lamtjes, +En al 't Gediert, die springen op, +En soeckens hares moeders mamtjes: +De Pilkes kruypen uyt de dop. +Maer een jongh Kindt leydt lange jaren, +En streckt sijn Ouders tot beswaren. +3. Als and're Dieren zijn gebooren, +En op de wereldt voort-gebracht, +Soo koom'se met haer kleedt te vooren, +Met pluymen, wol, of hayren vacht. +De mensch alleen heeft naeckte leden, +En and're moeten hem bekleeden. +4. Een Beest dat leeft oock sonder sorgen, + +Sonder arbeydt, en sonder sweet: +En 't sorght noyt voor den dagh van morgen, +Want altijdt staet sijn disch gereet. +Dit magh de mensche niet gebeuren, +Maer moet in 't werck hem schier verscheuren. +5. Natuur heeft oock een Beest geschapen +Bequaem tot tegenstant van strijt: +Want yder Beest dat draeght sijn wapen, +Daer mede dat het vecht, en smijt: +Maer d'arme mensch en draeght geen hoorens, +Geen klauwe, noch geen scherpe spoorens. +6. Een Beests veerdigh op sijn benen, +En afgeveerdigh tot de vlucht. +Een Vogeltjen dat vlieght daer henen, +Tot boven in de blauwe Lucht. +Maer een mensch is seer traegh van leden, + +En gaet heel log daer heene treden. +7. Een Beest leeft ook veel meer na reden +Als wel een mensche selver doet +Want het is in sijn staet te vreden, +Maer een mensch soeckt al meerder goedt. +En dit ontciert den mensch noch 't meeste, +Dat hy een dienaer is der beeste. +8. En wilt ghy van de kunsten roemen, +Die geschieden door 's menschen geest, +Soo sal ick u eens kunsten noemen, +Eerst uyt-gevonden van een beest. +En hy is van veel grooter eeren, +Die kunsten vindt, als diese leeren. +8. Wie heeft het mets'len eerst begonnen? +Het was een Swaeltien in sijn nest, +En hoe het Gaern moet zijn gesponnen, + +Leerden de spinnen alderbest. +En hoemen Landen moet Regeeren, +Dat mostmen van de Byen leeren. +10. En hoemen sijnen stem moet wringen, +In groven, en in fijnen tael, +Om alsoo goedt Musijck te singen, +Dat leerde ons de Nachtegael. +En oock de Aerde t' onder-mijnen, +Dat leerd ons Mollen, en Konijnen. +11. Een Pellicaen die leerd' het laten, +Door 't tappen van sijn eygen bloedt: +Een Oyevaer die leerd' ons vaten +Hoemen Klisteren setten moet, +Een Esel leerd' in oude tijden, +Datmen den Wijngaert moet besnijden. +12. En om door 't Roer een schip te stieren, + +Dat saghmen aen een Visch sijn steert. +In somma: daer zijn veel manieren, +Dat het Gediert ons heeft geleert. +Wat wil hem dan een mensch verheffen, +Daer hem de Beesten overtreffen. +
+
+
+ +Een bekeerde Sondaresse, Luc. 7. +
+Stem: Ga wereldts minnaer vliet. +GHy die aen lust, en weelt, +Lijf, Ziel, en tijdt verspeelt, +Komt siet hier aen een aerdigh beeldt, +'t Welck was als ghy, maer neemt voortaen, +Voor 't Hels, een Hemels wesen aen. + +2. Komt leert een nutte les +Van eenen sondares, +Een vuyle Hoer: maer onder des, +Sy krijght berou, en wordt het moe, +En neemt haer gangh na Christum toe. +3. Daer sy een tranen vliet +Wt hare oogen schiet, +Dat niet uyt list, maer uyt verdriet: +Wt d' oogen, daer de geyle min, +Noch onlanghs hield haer woonplaets in. +4. Het Hayr dat als een net, +Tot vangen was geset, +En menighs kuysheydt had besmet, +Daer mee veeghd' sy de tranen af, +Die sy in 't schreyen over gaf. +5. De soeten Balsem mee, + +Daer voor sy eertijdts dee +Haer selven salven, doet haer wee, +Dies sy 't besteedt tot Christi eer, +En stort het op sijn voeten neer. +6. Haer mondt tot kus besteet, +In minne heyl, en heet, +Die kuste oock, maer 't was uyt leet, +Die kuste oock, maer 't was uyt lust, +Een lust die in haer Godt berust. +7. In somma: wat sy vint, +Dat dul, en onbesint +De ydelheden had bemint, +Dat wendse om, en gaf het weer, +Tot een geschencken aen haren Heer. +8. En hy, die sy dit geeft, +Is vriend'lijck, en beleeft, + +'t Welck hy haer oock bewesen heeft: +Want t'wijl sy drupp'len tranen laet, +Soo geeft hy stroomen van genaedt. +
+
+
+ +Nieuwe-Jaers Liedt. +
+Stemme: Als 't begint. +HEt eerste daghjen in 't Nieuwe Iaer, +'t Welcke ons nu is verschenen, +Dat leert ons duydelijck, ende seer klaer, +Hoe snel ons leven gaet daer henen. +2. By dagen soo loopt het jaer ten ent, +By jaren verloopt het leven: +Wel dwaes is hy dan, die niet en bekent. + +Dat men het haest moet overgeven. +3. Maer wy die beter sijne geleert, +Wy vinden hier groote reden, +Nadien ons leven seer haestigh verkeert, +Om altijdt 't leven wel te besteden. +4. Seer menigh al in het jaer lest-le'en, +Swom oock in den poel der sonden, +De doot die ginger seer veerdigh mee heen, +En 't Graf heeft al haer vreught verslonden. +5. Wel wieje dan zijt, en laet u hert +Niet soecken wereldts genuchten: +Noch wilt oock mede in lijden en smert, +Niet al te seer, noch droevigh suchten. +6. Seer haestigh vergaet des wereldts soet, +En het suur, en bitter mede: +Maer leeft Godtsaligh, soeckt 't Hemelsche goet + +Dat blijft ons by, altijdt in vrede. +
+
+
+ +Des Heeren Wijn-bergh Ies.5:1. +
+Stem: Tweede Carileen. +'K Wil de liefste die men siet, +Singen voor, van mijn bemind' een Liedt, +Die een Tuyn, en Wijn bergh, op den kruyn +Heeft geplant, +Van den vetsten heuvel in het Landt: +Wat steen hy vondt, in den grondt, is terstont +Wt geret, +Goede rancken ingeset: + +En een want, om den kant, vast geplant: +En hy toogh +In 't midden op, een toorn seer hoogh. +2. En hy maeckt een wijn-pars-back: +En hy groef, en dolf hem om, en stack, +Ia hy wrocht, hem ter deegh, maer hy brocht +Geen gewin, +Hy socht vruchten, maer daer quam niet in: +Wat dat hy sagh, dagh aen dagh, hy en magh +'t Smaken niet, +'t Geen sijn Wijn-bergh aen hem biet: +Van natuur, wrang en suur, als een muur, +Dor, en hert, +Is de vrucht, die bekomen werdt. +3. Komt Ierusalems Vier-schaer, +Vonnist nu, en spreckt in 't openbaer, + +Of een man, sijn Wijn-bergh, meer doen kan, +Als ick die. +Heb bewesen, en geen vrucht en sie, +Wel waerom schengt? waerom brengt? waerom dengt +Hy niet toe, +Vruchten aen my, wat ick doe? +Of ick ga, vroegh en spa, soecker na, +Iaren langh, +Wat ick vind, dat is suur, en wrangh. +4. Wel aen Wijn-bergh, het besluyt +Is gevelt: Ick sal het voeren uyt: +Sijnen wout, ruck ik om, dat de kant +Niet belet, +Noch kan schutten, wat hem geern vertredt. +Dan sal dien thuyn, op den kruyn, van dien duyn +Soo verciert, + +Zijn vertreden, van 't gediert? +Ia hy sal, heel en al, door dien val, +Oyt en oyt, +Zijn verwoest, ende heel beroyt. +5. Daer sal niet meer wassen uyt, +Als onreyn, en schadelijck onkruyt: +Nimmermeer, sal een wolck, aldaer neer, +Wt sijn vat, +Lossen sijnen Hemels water-schat. +Desen wijn-gaert, quaedt vermaert, quaet van aert +Quaedt van stant, +Is het Israels vette Landt: +En ick wacht, of 't geslacht, vreughten bracht +Na mijn sin, +Doch ick vind, daer maer sonden in. + +Hollandt, daer aen de Heere gaf, +In't eerst veel goedts in t' lest veel straf: +Het eerst uyt gunst, het lest na waerd' +Is even soo, als dees Wijngaerd. +
+
+
+ +Des Heeren weldaden. +
+Stem: Geswinde Bode van de min: +GElijck Godt met sijn wijn-bergh dee, +In kana geplant, +Soo doet den goeden Godt oock mee, +Met ons Vaderlandt: +Want de Heer // heeft dit Landt, + +Tot sijn eer // door sijn handt +Met goede // vervult in overvloet. +Soo dat wijdt, en breedt, +Even als een kleedt, +Godts Barmhertigheydt, +Worde over 't Landt gepreyt. +2. In den Eersten quammer voort, +Inde duysterheydt, +'t Helder licht van Godes woordt, +Dat ten Hemel leydt, +Doe dit quam // uyt de lucht, +Daer op nam // strack de vlucht +'t Gespuys // en 't spoock van Babels huys. +Ende hier op strack, +Oock aen stucken brack, +Den handt, die de ziel + +Vast in Roomsche stricken hiel. +3. Doe al de wereldt dit aenschoud, +Maeckt sy haer bereydt, +Dat sy met Silver, en met Goud, +En meer kostlijckheydt +Vol van glans // die noyt dooft, +Eenen krans // op ons hooft, +Ten pronck // en tot verciersel schonck: +Yder Landt schonck wat, +Van sijn besten schat, +Alsoo wordt hier door +Holland, 's werelds schat-tresoor. +4. Het deense Bos, dat deel sijn hout +Hier aen Hollandt mee, +Daer van men duystent Schepen bouwt, +Swemmend op de Zee: + +Sulcken som // toe-gerust, +Seyld' romdom: yder kust +Die goot // sijn schatten uyt de schoot: +Want den Indiaen +Quam met silver aen, +Roodt gemenght met Gout. +Smanjen schonck haer wijn, en sout. +5. Vranckrijck dat gaf oock wat het kon: +Sweden koper sondt, +En al de Rogg, die Polen won, +Quam ons in de mondt: +Engelandt // pluysde sacht, +Met haer handt // 't Schaepjes vacht +Heel af // dat sy aen Hollandt gaf. +Somma, in het kort, +Yder Landt dat stort, + +'t Beste uyt sijn schoot +Yeder in de Hollandts' vloot. +6. En oock de Zee, seer grijs en blau, +Wt haer ingewandt, +Schonck Haringh, ende kabeljau. +En oock self Hollandt, +Word' een Hof // rijck in kruydt, +Alle stof // gaf het uyt, +En toond // dat Godes gunst daer woond' +Want daer vloeyd' een stroom, +Wit van melck, en Room, +'t Scheen dat yder Gras, +Een Fonteyn van suyvel was. +7. Doe nu dit door des Heeren gunst, +Hier quam by malkaer, +Soo quamen oock natuur, en kunst, + +Alle beyde daer, +Om dit Landt // in die staet, +Met haer handt // sijn cieraet +Heel kant // te stellen in sijn stant: +Dese maeckten rat, +Yder dorp een Stadt, +Yder Stadt een Rijck, +'t Rijck de wereldt heel gelijck. +8. Den Spanjaert door dees heerlijckheydt, +Worde soo verveert, +Dat hy voor onse voeten leydt, +Moordt-priem, spiets, en sweert: +En terstondt // men vernam, +Dat een hondt // word een Lam, +Daer staet // Hollandt in sijn cieraet, +En blinckt dat de son + +'t Nau verdragen kon, +Ia sagh hem schier blindt. +Siet dus heeft ons Godt bemint. +
+
+
+ +Hollandts suure Druyven. +
+Stemme: Soo langh is't Muysjen vry. +MAer doe ons Vaderlandt, +Door Godes milde handt +Dus rijck gezegent was, soo terghd'se hem tot wraeck, +En koren niet sijn dienst, maer Bacchus tot vermaeck. +Sy seyden noyt te saem, +Komt danckt des Heeren naem + +Die ons dit alles geeft: en of men rijkdom krijgt +Elc roemt sijn neersticheyt, terwijl hy God verswigt +3. En t'wijl elck een bedenckt, +Niet dat hem Godt dit schenkt, +Maer sijn selfs vernuft, en kloekheyt hem dit deed, +Hy 't niet tot Godes eer, maer eygen lust besteed +4. Hier door soo wordt het Landt +Vervult aen alle kant, +Met wulpsheyt, en met pracht: en na de rijckdom wast, +Wordt oock het Vaderlandt met sonden overlast. +5. En of sijn rijckdom staegh +Vermeerdert alle daegh, +d'Eergiergheydt, en nijt, 't hooveerdige gemoet +Die steeken t hooft noch op ver boven al sijn goet +6. De groote gane voor, +De kleyne volgen 't spoor, + +Dus gaense met malkaer, den wegh al na de Hel, +En die ten Hemel klimt, vint nau een met gesel, +7. Het gantsche Landt dat blinckt: +O neen! het Landt dat stinckt +Van vuyle hoovaerdy, van hoog-moet en van trots, +En smeert sijn vleken aen de beste kind'ren Godts +8. Die nu met sijn verstant +Door-loopt ons Vaderlandt, +'t Is of men nu ter tijdt, geen kind'ren Godts en siet, +Men siet de witte deught, de vreese Godes niet. +9. De Goddeloose klap, +De dulle dronckenschap, +Het geyle Hoer-gesangh dat sweefter over straet: +'t Is of men in het Hof, van 't snoode Romen staet. +10. En t'wijl het dus toegaet, +Soo klinckt op onse straet, + +De stemme van Gots woort, gepredikt in de Kerk +En roept: verbystert volck, verlaet u sondigh werck. +11. Dit is te onbeleeft, +Dat Godt sijn zegen geeft, +Dat hy u Landt met goet, en ghy met quaet vervult: +Voorseecker sijn toorn ontsteeckt om dese schult +12. Dus bromt het heyligh woordt, +En menigh die het hoort, +Die toont dat al de kracht, hier van, in hem verdwijnt: +En 't hoopken is seer kleyn dat nu als lichten schijnt +13. Ia self des Heeren dagh +Moet dienen tot gelagh, +En ongebondentheyt! Ia 't schijnt nu voor gewis, +Dat die des satans dagh, en niet des Heeren is. +14. Een gruwelijcke schant, +Voor u, O dertel Landt! + +Dat ghy door Hemels gunst, met zegen overstort +Vloeyt over in het Goud, en comt in deugt te kort. +
+
+
+ +Des Heeren plage over 't Landt. +
+Stemme: Treurt edel huys Nassou. +DOe nu ons Vaderlandt. +Dus tegen Godt aen kant, +En terghd' hem alle dagen,Ontstack oock God de Heer, +En sondt seer vele plagen, +Op dese lande neer. +2. De eerste trof de Staet, + Van Hollandts hooghste Raedt,     Anno 1650 + +Soo dat het Hof der Grooten +Verwerde in 't geheel, +En worde overgoten +Met twist, en met krackeel +3. Wt dese twist, soo quam +Een droeven Oorloghs-vlam, +Die tot den Hemel brande: + Ons Leger quam gegaen,    Den 29. + En rast in dese Lande,         Iulij 1650. +Ons eygen Koop-stadt aen. +6. Hier na noch meerder quam, +Want, den Oranjen stam, +Wt 't edel Hof Nassouwen, +Die al het Landt verheught, + Die worde om-gehouwen,        Den 6. + In 't beste van sijn jeught.        November + + 5. De woeste Zee, oock wordt     Anno 1650 +Door stercken storm geprot, + Die met sijn grijse golven,       Den 5. + Al schuymende aenkoomt,       Maert +En doet den dijck om colven,   1651. +Soo dat hy hene stroomt. +6. Godt sondt sijn plagen mee, +Tot onder 't domme Vee, +Dat schier vergingh in quellingh,   De Na somer + En meest van honger storf,            1651 +Door dien een vreemde swellingh, +Daer mondt, en voet door kort. +7. Den Hemel wort vergramt, +Soo dat hy brandigh vlamt, +En door de Sonne stralen +Versenghd' het Gras, en kruydt:     De Voor En + + En liet geen regen dalen      somer + Ter Hemel-sluysen uyt.       1652. +8. Oock vanght een Oorlogh an + Met Ons, en d'Engels-man,     Gepubliceert +Die met seer vele schepen,     den 2. + Gaet swemmen over Zee:        Augusti. +En geeft ons harde nepen, +En neemt veel schepen mee. +9. Hier door soo worde strack +De Neeringh kranck en swack, +Ia storf ten langen lesten, +En blies het leven uyt: +Ons goedt, sleept in sijn nesten +Den Engels man, tot buyt. +10. De doodt oock, Gods dienstknecht, +Die sijn bevel uyt-recht, + + Schoot sijn vergifte pijlen      Het geheele + Tot in des menschen hert,     Iaer 1652. +Soo dat in korte wijlen +Veel volck begraven werdt. +11. En eer dit Iaer was uyt, +Quam 's avonds in het zuydt,   In December + Een komeet te vooren.              1652 +Als of hy woud' bedien, +Dat Godt sijn strenge tooren, +Noch meer woud' laten sien. +12. En 't is oock soo geschiet, +Een Iaer vol van verdriet,     Anno 1653. +Qaem dese sterre volgen, +En heeft door tegen spoet +Verslonden en verswolgen, +Seer veel van Hollandts goedt: + +13. Ick 't alles niet verhael, +Maer onsen Admirael. + Een van de beste Grooten,           Den 10. + Verlooren wy doe mee,                Augusti +Van d' Engels-man geschooten,  Anno 1658. +By Egmont, in de zee. +14. Oock word in 't Landt gestiert + Een schadelijck gediert:          De gantsche + Veel duysenden van muysen,  somer + Ia een ontelb're som,               1653. +Die al het Gras af pluysen, +En wroeten 't Landt voort om. +15. Den Hemel sagh dit aen,     In septemb. + En schoot een vloedt van traen        1653. +Al weendend' uyt sijn oogen, +En maeckt het Landt, een zee: + +Soo dat de Boeren toogen +Na huys toe, met haer Vee. +16. En oock een storm ontstack, + Die menigh huys verbrack:       Den 7. Ianuarij +Veel schepen zijn bedorven,     1654 +Die op de woeste Zee, +Haer spitse masten korven, +En noch vergingen mee. +17. En in dees storm by nacht, +Daer niemandt op en dacht,     Item. +Soo quam de Brandt verrasschen +De Rijp, dat schoone dorp, +Soo dat het in der asschen, +In korten neder worp. +18. Soo dat ons dagh op dagh, +Nu treffe slagh op slagh: + +O! ghy vervloeckte sonden, +Door u is dit geschiedt, +Dat men hier soo veel wonden, +Door dese slagen, siet. +
+
+
+ +Genees-middel. +
+Stem: O jonge jeught bly-hertigh. +MEn hoort nu Hollandt klagen, +En suchten om dees plagen, +Om oorlogh, en om dieren tijdt: +Maer om hier van te zijn bevrijdt, +Soo moetwe in dees dagen, +Ons sonden eerst verjagen, + +En maecken ons dien vyandt quijt: +2. Godts toorn is niet ontsteecken +Op menschen, maer gebreecken: +En buyten sonden is de mensch +Des Heeren lust, en herten wensch: +Wel laet ons die dan kelen, +Soo sal ons Godt weer heelen, +En toonen ons sijn gunst allensch. +3. Als Seba vlucht in Abel, +Quaem David met sijn sabel, +En steld hem soo geweldigh aen, +Als of hy Abel woud' verslaen: +Maer doe na sijn behagen, +Die seba was verslagen, +Is hy van Abel afgegaen. +4. Soo oock, O Hollandts Borgers! + +Gaet, stelt u tot verworgers, +En keelt op sonden, dagh aen dagh, +Elck een soo vele als hy magh: +En Godt sal dit u slachten, +Veel aengenamer achten, +Als Offerhand, van beesten slagh. +5. Oock moeten wy in't midden +Der plagen, Godt aenbidden, +Dat hy ons niet na weerde loon, +Maer door sijn gunst genade toon: +En dat hy in dees dagen, +Ons vry maeckt van dees plagen, +En ons met sijnen zegen kroon. +6. En als hy na 't verschoonen +Der plagen ons doet kroonen +Met sijnen rijcke zegeningh, + +En schenckt ons weder alle dingh, +Soo moetwe Godt van boven, +Voor sijn genade loven, +Daer van ons landt sijn schat ontfingh. +
+
+
+ +De kleyne Werelt heeft een Iaer: De Groote, duysent na malkaer. De Lente, +
+Stem: O Flora ydel is u roem. +ALs ick die son verhoogen sie, +En sie hem op, na boven klimmen, + +Wt de Zuyer kimmen, +En door kracht van die, +Het watter, de Lucht, en het Veldt, +In eenen nieuwen Ieught gestelt, +En wassen daeg'lijcks an, +Soo denck ick daer op van: +2. De groote wereldt die gaet voor +De kleyne wereldt volght sijn schreden, +En gaet hem na treden, +In het selve spoor +En dit alleen is het verscheel, +De kleyn gaet, eens, de groot gaet veel. +De kleyne in 't begin, +Die brenght de Lenten in. +3. Dan loopt de Son seer snel om hoogh, +En doet het gantsche Velt in vreughde, + +En een soete jeughde +Vertoonen voor 't oogh: +'t Is even soo, gelijck het soet, +Dat in de Kindtsheydt hem op doet, +Wanneerse in haer Ieught, +Ons toonen groene vreught. +4. Wat teyckens men van vreucht oyt sagh, +Dat van het Veldt, of van de Boomen, +Wy sullen bekomen, +Komen voor den dagh: +Soo gaet het mede met de Ieught, +Men siet 't beginsel van de deught, +Of oock, met siet het zaedt, +Van het volgende quaedt. +5. Ick sie de Son noch hooger gaen, +En schild'ren 't Velt met vele Bloemen, + +Sy zijn niet te noemen, +Die men nu siet staen: +Oock menigh Bloemtjen roodt, en wit, +Op onse Ieught haer wangen sit +Of glinst'ren doormalkaer, +In 't blinckend Goudt-geel haer. +6. Ick hoore nu oock dagen langh, +Een soet, en aengenaem gewemel, +Alsoo dat den Hemel +Klinckt door een gesangh, +Van duysent stemmen te gelijck, +Al sonder voys, of wildt musijck +Alleen gedicht uyt vreught, +Gesongen uyt geneught. +7. Siet hier een aerdigh tegen-beeldt, +Aen het gesaagh, en aen kelen, + +En het lieflijck spelen, +Dat de jonckheydt speelt: +Soo aengenaem, dat al het soet, +Van Vogels daer voor wijcken moet, +Als sy het aerdtsche dal, +Besuyck'ren met geschal. +8. Maer t'wijl de dertjes allegaer, +Nu verkiesen een gesellinne, +De mensch oock uyt minne, +Kiest een weder-paer, +Op dat hy ins dees soete mey, +In soete lusten, met sijn bey, +De wereldt meer verciert, +Met jongh, en soet gediert. +
+
+
+ + +De Somer. +
+Stemme: L. Orangie. +ALs hem de son begeeft, +Te wand'len door de kreeft, +Soo dooft te met het geylste groen: +En soo verandert het saysoen +Der tijden // en lijden +Niet meer het soet, +Dat ons de lente broet: +Elck kruydtjen // en spruyten +Sijn groenste cieraet dan verminderen doet. +2. Aldus oock openbaert +Den mannelijcken aert, + +Als dese komt dan gaet sijn gangh +De lossen vreught, en soeten sangh, +Het wesen // voor desen +Haer aengenaem, +Wordt haer dan onbequaem: +De grillen // die stillen +Allenghsjes, en dooven weder te saem. +3. Al 't gewas, en al 't goedt, +Dat heer de wereldt voedt, +Of haer geneest, of oock verblijdt, +'t Wast alles in des somers tijdt, +Dan toone // de schoone +Landen haer vrucht. +De soete somer lucht, +Die voedt haer // en doet haer +Vruchtbaer groeyen tot een yders genucht. + +4. Soo oock, een yder saeck, +Tot nut, en tot vermaeck, +Wordt van de mensche uyt gewracht, +Als hy is in sijn levens kracht: +De Reden // De Leden, +Siel, en Lichaem, +Zijn dan sterck, en bequaem, +En maecken // de saecken +Door de geheele wereldt aengenaem. +5. Maer noyt de son stil staet, +In d' alderhooghste graet: +Maer soo hy quam na boven treen, +Soo stapt hy weder na beneen: +Dan doene // de groene +Velde, den glans +Verdooven, van haer krans: + +Men vindter // en winter, +Nu noodige vruchten al rijp, en gans. +6. Soo oock het stercke bloedt, +Dat is maer als een vloedt, +En neemt haest wederom sijn keer, +En dooft de mensch allenghjes weer: +Dan winnen // de sinnen +Niet meerder aen, +Maer haesten af te gaen: +Maer 't gene // voorhene, +Is geleert, wort dan met vruchte gedaen. +7. Die 't maeyen in den Oost, +Geheel verwareloost, +En niet met allen in en haelt, +Wordt veeltijdts met dit loon betaelt: +Als tijden, van lijden, + +Hem komen aen, +En hem met honger slaen, +Dat klagend // al vragend +Hy seggen sal: Och wat heb ick gedaen? +8. En die hem niet begeeft, +Als hy in krachten leeft, +Om dan te wercken in den Oost, +Daer Godt uyt deelt sijn soeten troost, +En slagen // en plagen, +Hem treffen aen, +Soo sal hy onder 't slaen, +Dan kermen: Och ermen +Wat heb ick al voor een dwaerheydt begaen +
+
+
+ + +De Herfst. +
+Stem: Treurt edel huys Nassouw. +ALs ick den Herfst aenschou, +Die met een nare rou, +De Velden, en de Landen, +En al de Boomen stroopt, +Soo dat hy alderhande +Verciersel heel af sloopt: +2. Soo denck ick daetlijck om +Den hoogen ouderdom, +Die nu geheel, en allen, +Des levens groen, en loof, +Verliest, en laet ontvallen, + +En wordt soo dor, en doof. +3. Wanneer den Herfst het Veldt +Becomt in sijn gewelt, +Het set hem strack na treuren: +En 't somers groen vertreckt, +Terwijl met grijse kleuren, +Het landt hem overdeckt. +4. Den mannelijcken glants, +Vergaet oock heel, en gants, +Ontrent de oude jaren: +De schoonheydt die verdooft, +Terwijl de grijse haren. +Aenkippen op het hooft. +5. En alderhande kruydt, +Dat in de Lente spruyt, +Of somers is gewassen, + +Dat heeft den Herfst dan in +Syn Schuuren, en sijn Kassen +Tot nut van 't Huys-gesin. +6. Soo oock, wat voor deught, +Geleert is in de Ieught, +Of in de manlijckheden, +Dat is dan in 't gemeen, +Van hem die leefd' na reden +In d' ouderdom by een. +7. Wanneer de Son gaet leegh, +Dan treffen ons ter deegh, +De pijlen van de koude: +En dat wy door dit kruys +D[u]s aengeprickelt, soude +Verlangen na ons huys +8. Als dan een mensch gevoelt, + +Dat hem het bloedt' verkoelt, +De krachten hem begeven: +Soo noem hem vry geen mensch, +Soo hy na 't Hemels leven, +Niet hoopt, met al sijn wensch. +
+
+
+ +De Winter. +
+Stem: Amarilli mia bella. +WAnneer de Son sijn stralen +Van ons ontreckt, en op den moor doet dalen, +Wordt het in onse palen +Geheel ontciert: Daer blijft noch kruyt, noch lover +In al de Velden over + +Dan valt de natuur, gelijck als in 't beswijmen: +En de Landen // als de zanden // aen de stranden, +Wit berijmen. +2. Wanneer ick hier op achte, +Soo valt my in, terstont in mijn gedachte, +Dus is 't met 's Menschen krachte: +Als Godt de Lucht, die wy ter Neus in snuyven, +Op sijn woordt, doet verstuyven, +Dat is al 't geen, dat dus lang noch heeft geschenen +Voor sijn sonne // nu verwonnen // nu verslonnen, +Nu verdwenen. +3. De Boomen op de Velde, +Die door 't gewicht der vreuchten neder helde +En wat ons vrucht bestelde, +Staen nu gestroopt, en hebben 't al verlooren: +De wat'ren zijn bevroren, + +Dat onlangs vloeyd, is nu harder als een Nagel. +'t Velt gaet schuylen, als in kuylen // door der buyien +Sneeuw, en Hagel. +4. Soo oock, de mensch na 't leven, +En sal niet meer aen yemandt teecken geven, +Van 't geen hy heeft bedreven, +'t Is alles uyt: het bloet dat door sijn leden, +Heeft op en neer gereden, +Dat stremt tot ys: dus comt de vorst in hem selven, +En dan spoetmen // want dan doetmen // ja dan moetmen +Hem bedelven. +5. Daer rust hy in sijn Kamer: +Geen Koninghs-hof is tot de rust bequaemer: +Als de straf met sijn Hamer, +Van Godt gestuurt, komt over 't Landt te sweven +Om menigh slagh te geven. + +Soo rust hy daer, om de straffe te ontschuylen, +Vry van plage // vry van slage // vry van klage +Vry van huylen. +6. Oock sal hy daer niet wesen +Voor alle tijdt: maer als komt opgeresen, +Die Soone, die door desen +Ver boven Son en sterren is geweken, +En van daer sal voort breken, +Met soo een glans, die dees Son sal doen vertrecken, +Dan sal desen // opgeresen // wesens wesen +Hem op-wecken. +7. En brengen in de woonningh, +Daer 't eeuwigh blincken aen hem geeft vertooning +d' Heerlijckheydt van dien Koningh +Die sterck bemuurt, met der Engelen scharen, +Dus eeuwigh, sonder jaren + +Leeft sonder end. Geeft o Heer ons al te samen, +Dat na 't sterven // wy verwerven // 's Hemels erven, +By u Amen. +
+
+
+ +Een Weeck toont ons in volle daet, Hoe dat het met ons leven gaet +
+Stem: Wel dus bedroeft Ionckvrou? +WIl yemandt in het kort +Eens sien des menschen leven, +Die kan 't haest sien: want 't wordt +Ons in een Weeck beschreven, +De vreught, en doefheydt door malkaer, + +Ia 't leven hier, en oock hier naer, +Blijckt alle weecken klaer. +2. Een gantsche weeck bestaet +Wt daghen, ende nachten, +Wanneer den eenen gaet, +Treedt d' ander op sijn wachte: +En t'wijl den dagh volvoert haer plicht, +Soo toont sy ons een helder licht: +De nacht een naer gesicht. +3. Dus is ons leven mee +Te saem in een geweven, +Van onrust, en van vree, +Van droef, en vrolijck leven. +'t Is altemet een ander stuck: +Somtijds beschijnt ons het geluck, +Somtijdts ontmoet ons druckt. + +4. Een weeck in haer aenvangh +Brenght ons den arbeydt mede, +En geeft in haer voortgangh +Ons seer vermoeyde leden: +Den arbeydt, met haer ommeslagh, +Doet, datmen na den Saterdagh +Hier wel verlangen magh. +5. Dus vanght oock 't leven aen +Met veel onrustigh woelen, +En doet ons in 't voortgaen +Veel ongemack gevoelen: +Soo dat men met verlangen groot, +Magh hopen Chris'lijck na de doodt, +Die ons treckt uyt dees noot. +6. Maer na een droeve ry +Van dagen vol van woelen, + +Komt ons den Son-dagh by, +En doet ons rust gevoelen +Dan werckt de ziele hare plicht, +En wordt in Godes huys gesticht, +Door 't soete Hemels licht. +7. Dit beeldt ons 't leven af +Dat Godt de ziel sal geven, +(Als 't lichaem rust in 't graf. +By hem, in 't eeuwigh leven +Godts lof is daer haer daegh'lijcks liedt, +Terwijl dat elck een vreught geniet, +Als hier noyt oogh en siet. +
Den dagh beelt ons het leven af, +De nacht de doot, en 't duyster graf +
+
+
+ + +Den Dagh. +
+Stemme: Edel Karsouw. +WAnneer oprijst // de Sonne in den morgen, +En op gaet in sijn kracht, +Hy ons aenwijst // het leven vol van sorgen, +Van 't menschelijck geslacht: +En soo men betracht, +Het menschelijcke leven, +Siet men 't klaer // en by malkaer, +Op eenen dagh beschreven. +2. De morgen-stont // die met sijn gouden vlamme +Het gantsche Landt vergult, +toont ons de mont // des kints, dat an de mamme + +V hert met vreught vervult, +Als ghy aensien sult +Sijn soete bolle kaecken: +Sijn gesicht // en 't morgen-licht, +Zijn twee gelijcke saecken. +3. Het hooger gaen // der Son, in de' eerste uren, +Tot hem de middagh keert, +Dat wijst ons aen // den aenwas der nature +Die dus haer kracht vermeert. +Sooje meer begeeert, +Ghy sult Bruyloft na 't trouwen, +In 't onthael // van 't middagh-mael, +Gneughelijck aenschouwen. +4. Maer desen stant // en blijft niet in een wesen, +De sonne daelt haest neer, +En sijnen brandt // en heete vlam na desen, + +Die dooft allenghsjes weer. +Oock de mensch, goe seer, +Hy is verciert in allen, +Sal nochtans // sijn schoone glans +En kracht, te met vervallen. +5. Maer als de son // aleer hy komt te scheyden, +Gaet na beneden toe, +Wie oyt begon // te wercken, en arbeyden, +Die wordt dan mat, en moe: +Elck gaet na huys toe, +En laet sijn arbeydts saecken, +Om 't gemack // dan onder 't dack, +In d' Avondt-stondt te smaecken. +6. En even dat // sietmen oock soo geschieden, +Op 't hooghst van 's levens trap, +Dat moed en mat // in d'ouderdom, de lieden, + +Haer krachten worden slap +Maer de wetenschap +Van onmacht, en swackheden, +Wijst haer aen // om dan voortaen, +Te rusten hare leden. +
+
+
+ +De Nacht. +
+Stem: Carileen ay wilt u niet verschuylen. +Als in koelt // de nacht komt overkleeden +Met veldt, met een swarten schijn, +Soo daelt dan op ons weer, +Van boven neer +De soete slaep // daer door wy in ruste zijn. + +Dan en voelt // men in de gantsche leden, +Geen vermoeytheydt, noch verdriet, +Maer al het ongemack, +Dat 's daeghs ons stack, +Dat is door de soete slaep geheel te niet. +En op dit sachte +Op-geschudde dons, +Troulijck over ons. +En laet // het quaet // van d' Helsche macht +Niet toe // dat het doe // het gene daer 't na tracht. +2. 'k Hier in vind // na 't leven afgebeeldet, +Den doodt, en het geen sy doet. +Sy komt in swarten schijn, +Van smert en pijn, +Maer onder dat swert schuylt oock een ruste soet + +Die Godts kindt // altijdt sijn smerten heeldet, +En maeckt dat Godts volck vergeet +Des levens ongeluck, +Verdriet, en druck, +Droefheyt, en ongeval: Kortlijck, al haer leet. +Die Israel hoedet, +Self oock in der nacht, +Wanneer als woedet +Des Satans geslacht, +'t Welck meent // 't gebeente te rooven dan, +Betemt, hy, en demt // het, dat het niet en kan +3. Als dus is // de mensch in 't graf gelegen, +(Even of hy in der nacht, +Op 't bedde sacht gespryt, +Was neer geleydt, +Daer hy in stille rust na de morgen wacht, + +Om seer fis // verquickt, hem te bewegen: +De verwacht oock eenen dagh, +Daer in hy los geret +Wt des doodts net, +In een beter leven hem begeven magh +Een sulcken leven, +Dat een nieuwe jeught, +Aen hem sal geven, +Vol van soete vreught, +Daer oyt // noch noyt // den ouden dagh, +Het soet // van het goedt, verderven kan, noch magh +
+
+
+ +Hemelsche Vreughde. +
+Stem: Als Boecxvoetjen speelt. + +DE zielen die vry van het lijdende vleys, +Ontbonden, van sonden, gaen trecken op reys +En d' Engelen schoon // uyt d' Hemelsche troon, +Dan dragen by Godt in sijn hoogh Paleys. +2. Die genieten aldaer het salighe goedt, +En schincken, en drincken, het Hemelsche soet. +En singen Godts lof // in 't Hemelsche Hof, +Vol salige vreughden in haer gemoedt. +3. Daer sietmen geen Sonne, en nochtans geen nacht +Daer vliete, noch schiete, geen tranen noch klacht +Daer licht haer geen vlam // haer Keersse is 't lam +Dat voor haer benden hier is geslacht. +4. Daer vreestmen geen vyandt, die ruste verstoort +Van buyten, noch sluyten van binnen geen poort +Geen lijden, noch pijn // en salder in zijn, + +Soo datmen geen kermen, noch klagen hoort. +5. De zielen oock self in den Hemel geleydt, +Die blincken, door 't schincken van d' Heerelijckheyt: +Soo datse daer zijn // in heerlijcker schijn, +Als 't schijnsel der Sonne, alhier verspreyt. +6. Soo dat al het blinken van koningen pracht +Die troonen, of kroonen, op aerden aenbracht, +Wanneer men 't gelijckt // met 't gene daer blijckt +'t Is niet met allen, by 't selve geacht. +7. Als nu maer een droopje de Heere ons doet +Hier smaken, wy raken terstondt in 't gemoedt +Vol vreughde daer van; wat sal het zijn, dan +Als wy sullen smaken de gantsche vloet? +8. 't Is seeker dat niemant alhier op der aerdt +Kan vatten, de schatten by Gode bewaert: +Hoedanigh, hoeveel // sal wesen dat deel, + +Wordt noyt ter degen beneden verklaert. +9. Dit leven dus heerlijk, daer in men den Heer +Daer boven, sal loven, en singen hem eer +Dat streckt hem seer wijt // ja buyten de tijdt, +En daerom sal 't eyndigen nimmermeer. +
+
+
+ + + + + +Op de Vrede Gemaeckt tusschen de E. Heeren Staten deser Landen, ende den Protector van Engelandt. Gepubliceert den 18. Mey 1654. +
+Stem: Wilhelmus van Nassouwe. + +HEf op u hert en handen, +En danckt den goeden Godt, +O vrye Nederlanden, +Voor u geluckigh lot: +Godt heeft ons in ons dagen +Met zegen overstort, +En wederom sijn plagen, +En slagen, opgeschort. +2. Wy hebben langh tevoren, +Door sonden, boos en quaedt, +Ontsteken Godes tooren: +Maer hy rijck in genaedt, +Die doet ons eerst waerschouwen +Eer hy ten vollen slaet, +Op dat ons soo mocht rouwen +Ons krijtende misdaedt. + +3. Godt sondt na onse Naesten +Een wer-geest, met onvreed, +Die aldaer in der haesten +Het sweert trock uyt de scheed, +Daer d'Engels-man mee snede +Het oudt verbondt in twee: +Soo dat hy al nam mede +Wat Hollandt stierd' op Zee. +4. Wat tonge kan nu melden +'t Geen als doe is geschiedt? +Want Tromp' het hooft der helden, +Daer door sijn leven liet. +De Neeringh-rijcke Steden, +Die worden neeringhloos: +De Burgery onvreden, +Door schaed op schade altoos. + +5. Dit was Godts volck een jammer, +Een droefheydt, en hert seer: +En hier uyt oock soo quammer +Veel suchtens tot den Heer: +De stemme veler volcken +Quam daeghlijcks voor Godts troon, +Daer drongh tot door de wolcken +Een roepen, Heer verschoon. +6. En Godt, die in sijn handen +Het hert der Vorsten draeght, +En daer door dese Landen +Twee Iaren heeft geplaeght, +Die doet nu weder staken +Den trots van ons Vyandt, +Alsoo dat hy gaet maken +Een Vrede met Hollandt. + +7. 't Is wel een gulde strale +Daer door de nacht verdwijnt +Die uyt des Hemels sale +Op des Landen schijnt: +Nu gaen de Nederlanden +Weer swanger van vermaeck, +Terwijl men sluyt in banden +De moed-wil, en de wraeck +8. Dus doet ons Godt oprijsen, +Gelijck als uyt het Graf +Wie soude hem niet prijsen, +Die ons dit alles gaf? +Het is de handt des Heeren +Die alleen wonder doet, +Die 't alles kan verkeeren, +En schept het quaedt, en 't goedt. + +9. Maer lieve Heer der Heeren, +Bescherm-Heer van ons Staet, +Wilt voortaen t' uwer eere +Ons kroonen met genaedt: +Siet niet aen ons gebreecken, +Waer door wy menigh-werf +V tergen, om te wreecken +Ons quaedt, tot ons verderf. +10. Breeck af de snoode lagen +Tot hinder van ons Staet: +Och Heere wilt verjagen +Al 't geen niet recht en gaet: +Ghy hebt ons vreed' gegeven, +Geef daer u zegen mee: +En geef ons daer beneden +Voor al met u goe vree. +
+
+
+ + +Vermaninge aen de Nederlanden, by occasie van de teghenwoordighe Vrede. +
+Stem: Geklaeght zy u o Heer der Heeren +WAeck op, verstockte Nederlanden, +En volgh soo niet u boose lust, +Eer Godes toorn begint te branden. +En set in vlam u sachte rust: +Godt stelt ons vele teeckens vooren, +Daer door dat hy +Geduurigh roept in onse ooren: +Keert u tot my. +2. Onlangs dreyghd' hy ons door veel plagen +En toond hem gram in alle ding + +Maer heden nu, in onse dagen, +Weckt hy ons op door zegeningh: +Hy heeft sijn milde handt ontsloten, +En niet gebeydt, +Maer haestigh op ons neer-gegoten +Sijn goedigheydt +3. Dit doet hy om ons op te wecken, +En af te leyden van het quaed, +En door sijn goedt ons te trecken +Met sachte koorden van genaed. +Maer kan dit ons nu niet bewegen +Tot beterschap, +Soo sal hy hem in toorne tegen +Ons stellen t' schrap. +4. Om tot den gront ons te verderven, +Gelijck een vyer dat vreeslijck vlamt + +Waer sal dan 't schepsel troost verwerven, +Als dus den schepper is vergramt? +Voorwaer in Hemel, noch op Aerden, +En isser dan +Niet een soo sterck, noch groot in waerden +Die duuren kan +5. Ia alle scheps'len die wy vinden, +Dat zijn al dienaers onder hem, +Den Hemel, Aerd, en Zee, en winden, +Staen strack bereydt op sijnen stem. +Sijn pijlen heeft hy noyt verschoten, +En nimmermeer +Zijn sijn Fiolen leegh gegoten, +Van boven neer. +6. 't Is dwaesheyt van ons op te maken, +En teghen Godt te kanten aen: + +Want als sijn toorne is aen 't blaken, +Soo kan geen mensch voor hem bestaen: +Wel laet ons dan voor sijne slagen +Seer zijn vertsaeght, +En onse sonden van ons iagen, +Om 't welck hy plaeght. +7. Wy hebben ons met Godt verbonden +Tot een parthy van sijn Vyandt, +En hy voert oorlogh met de sonden: +Wat doen die dan in't Vaderlandt? +Soo wy dien Vyandt niet verdrijven +Wt onse Landt, +Hoe kan met Godt ons Vni blijven +In sijnen stant? +8. Wel laet u dan, o Landt waerschouwen, +'t Wijl't noch tijdt is om af te staen, + +Eer Godt om dese quaede trouwe +Ons als Rebellen komt te slaen. +Want hy, wiens oogen zijn als Sonnen +Op yder plaets, +Die sal niet van ons lijden konnen +Soo vele quaedts. +
+
+
+ + + + +Veldt-Sangh, Op 't vreuchtbare ghewas in Iulius 1654 +
+Stemme: Nere, schoonste van uw' gebuuren. + +DOe ick onlanghs de ruyge Velden +Met mijnen gladden Zeyn beschoer, +Gingh dus mijn keel Godts goedtheydt melden, +In 't zegenen van yder Boer: +2. Als Godt den Hemel doet verhooren, +En d' Hemel weer het Aerdtsche-dal, +En d' Aerde dan de Wijn, en 't Kooren, +En 't Koorn de menschen over al, +3. Soo komt in haest des Heeren zegen, +Met rijcke schatten, veelderley, +En laeft ons menschen, als de regen +Het groene Kruydt doet in de mey. +4. Met reden mogen wy dus seggen, +In 't schoone Dorp van Wervershoof, +Daer men onlanghs het Veldt sagh leggen, +Seer mager, schrael, en dor, en doof. + +5. In 't alderschoonste van de meye, +Doe was het landt noch wit, en swert, +Men sagh nau groene tusschen beyen, +Nau Beest op 't landt gevonden werdt. +6. Een yders hert was heel besloten, +En toe-geschroeft door 't swaer verdriet, +En 't Landt met plagen overgoten, +In 't welck men Godes toorne siet. +7. Wy sagen klaerlijck voor ons oogen +Een grooten druck, en swaren noot, +Ten zy dat Godt uyt mede doogen, +Ons weer sijn milden zegen boodt. +8. Godt doet oock haest sijn heyl weer toonen, +Midts hy ons op het spoedighst helpt, +En met sijn zegen ons doet kroonen, +En met veel gaven overstelpt. + +9. Het Landt, met jeughdigh gras besproten, +Was onlanghs slijck, maer staet nu groen, +Midts duysent duysent jonge loten, +Haer in der haest vertoonen doen: +10. Die tegen al 't vernuft, en reden, +Opschieten haest, men weet niet hoe: +Soo dat de gladde koeyen treden +In 't Gras, by na aen d' ooren toe. +11. Het hoy, dat men schier most opwegen +Met al ons geldt, wanneer men 't kocht +Heeft ons de Heer door sijnen zegen, +In overvloedt nu toe-gebrocht. +12. Hy heeft ons Koorn, en Terruw geschonken +Die d' Acker siet, het is genucht: +En onse Boomen staen en proncken, +Met alderhande leck're vrucht. + +13. Dus toont de Heer te met zijn krachten +In wonder wercken, ongemeyn: +Doe Simson woud' van dorst versmachten, +Maeckt Godt een kin-back een Fonteyn. +14. Elias, in gebreck van spijse, +Kreegh door een Raven sijnen kost, +En op een wonderbare wijse +Heeft Godt Samaria verlost. +15. Dus werckt de groote Godt van boven, +Op dat wy in voorspoedigheydt, +Niet ons vernuft en souden loven, +Maer alleen sijne goedigheydt. +16. Wy dancken u dan, Heer der Heeren, +Voor al 't geen dat ons landt ontfingh: +En wilt ons voort, tot uwer eeren, +By blijven met uw zegeningh. +
+
+
+ + +Passien, en Reden. +
+Stem: Cloris aen de Water-stroomen, +DIt is licht voor elck te lesen: +Sonder Passien te wesen, +Dat is min zijn als een Beest: +Maer soo wie hem heeft begeven, +Om na Reden niet te leven, +Die en heeft geen menschen geest. +2 Daer was noyt yemandt op Eerde, +Diese alle bey ontbeerde, +Maer wel menigh mister een: +Daerom is het wel een zegen, +Diese beyde heeft verkregen, + +En de Passy stuurt door Reen. +3 't Strijdt de Reden niet en tegen, +Dat de Passijen haer bewegen, +Maer dat sy gants qualijck gaen: +Daerom moet de siende Reden +Voor de blinde Passy treden, +Leyden die te rechte aen. +4 Ick wil droef, en vrolijck wesen, +Wanneer als tot beyde desen +My de siende reden wijst; +Maer ick wil my noyt verblijden, +Noch niet suchten in mijn lijden, +Dat het boven Reden rijst. +5 Ick wil soo mijn vreughde stieren, +Dat daer door in geen manieren +My een ongeluck aentreft, + +Ick wil alsoo matigh treuren, +Dat daer door noyt sal gebeuren, +Dat mijn leet daer door verheft. +6 Dat in ons de Passien leven, +Kan aen niemandt teecken geven +Van een dwaes, of slecht verstant: +Maer die ons te kennen geven, +Dat sy sonder Reden leven, +Zijn de Sotste die men vandt. +
+
+ + + + + + +Bruylofts-geschenck Op 't Huw'lijck van De. Hubertus van der Meer, Predicant tot Wevershove. + +Met Iuffr. Geertruyde Maria Bailly. Getrout tot Amsterdam den 25. Augusti 1654. +
+Stemme: Al wat men hier in dese wereldt siet, +ALs van der meer op Eng'le-vleug'len vloogh, +Sijn heyl'ge Ziel ontlast van teug'len, toogh +Ten Hemel-waert, en sagh in 't hooge koor, +Het Hemels-huys met geest'lijcke oogen door, + +2. Daer yder ziel Godts lieflijckheden siet: +Dus quam sijn geest alhier beneden niet, +Als dan alleen, wanneer door 't schijnent' licht +Van Godes wordt, hy 't volck tot sijnend sticht. +3. Maer t' wijl om hoogh, hy heen en weder vloog, +Hy eens't gesicht na d' Aerde neder boogh, +Daer hem een glants als van veel Peerels t'saem + In d'oogen blonk, beneen uyt's Amsterdam Werelts-kraem +4. Dies hy om laegh gelijck een Sterre schiet, +Na 't lieve licht dat hy van verre siet: +Maer doe hy nu by dese Vlam-ster quam, +Doe was 't Bailly' het puyck van Amsterdam. +5. Een maegt wiens deugt meer als cieraden blonck, +Daer aen Natuur al haer weldaden schonck: + +Wiens lieflijck oogh van minlijckheden buurt, +'t Welck sy alleen na deught en reden stuurt +6. Wiens kuyssche oor noyt stem ontfangen most +Wanneer d'ontucht haer swang're wangen lost. +Wiens tongh bewijst, dat waerlijck dese maeght +Geen aerdts, maer self een Hemels wesen draegt +7. Doe van der meer dit puyck der jeught vernam +En hy 't gesicht van hare deught bequaem +Wiens glants dat self een Hemels schoonheydt was +Is hem door dese ongewoonheydt ras +8. Een vuur ontfonkt, een suyv're minne-vlam +Die door't gesticht van dees vriendinne quam. +En t'wijl sijn ziel tot haer getogen werdt, +Sprack hy haer aen uyt een bewogen hert, +9. Met soo een tael waer uyt sy klaerlijck las +Dat dat de stem der minne waerlijck was. + +Dies sy (van aerdt gelijck als hy gestelt) +Oock word geboeyt en vast, door 't vry gewelt, +10. Van minne dwangh, die stelt door eenen vlam +Die van de deugt des minnaers hene quam +Haer hert in lichten vlam, door minne-brandt: +Daer smelt haer bey het hert by 't ingewandt, +11. Gelijck te met de held're witte snee +Door Sonne-schijn of and're hitte dee, +Den Hemel die sagh dese brandt opgaen, +Dies stack sy daer terstondt de handt op aen, +12. Daer sy die twee te samen mee men'len dee +Ia self ziel te saen dee streug'len mee. +O waerd gespant dat nu in soeter vreught, +Op 't Echte bedt uw' lusten boeten meught, +13. En met malkaer, in minne, plegen 't soet, +Dat al de vreught, en lust, opwegen doet: + +Treedt vrolijck heen, na 't bruylofts Liedekant, +Alwaer de min haer lieflijckheden plant, +14. En doet op-schieten, lusten voor u bey. +Maer stil mijn pen: my dunckt ick hoor een Rey, +Daer yder keel klinckt als een fijne Fluyt, +Op d' Echt van van der meer en sijne bruydt. +
+
+ +Stemme: Maskurade, Of: 't Visschen moet wel vreughde baren. +OM de Echt van dees Gepaerde, +t' Saem gesmolten in haer ieught, + +Zijn de Hemel, en de aerde, +Beyde vol van soeten vreught. +2. 't Segen-rijcke wervershove +Wordt met Hemels glants bedeckt, +'t Gaet den Amstel-Staedt te boven, +Daer de Sonne van vertreckt. +3. Wat is 't, of haer blauwe Toorens +Door wolcken henen gaen, +En sy met haer spitse hoorens +Dreyght te stooten aen de maen? +4. Al haer glants sal heel verdwijnen, +Onder 't Rou-kleedt van de nacht, +Daerm' in Wervershoof het schijnen +Van twee held're Sonnen wacht. +5. Driemael Heyligh, eeuwigh wesen, +Stichter van den Echten-staet, + +Laet nu bloeyen over desen, +Eenen stroom van Honigh-raedt. +6. Laet de Bruydt in't Huw'lijck wesen +Eenen Wijn-stock daer, o Heer +Vwe Kercke van magh lesen +Menigh iongen van der meer. +7. Die uw volck noch mogen leyden, +Door uw waerheydts held're schijn, +Als haer ouders, alle beyden +By u al ten Hemel zijn. +8. Geef oock dit, O Godt daer boven, +Tot ons heyl, en tot uw' lof, +Dat sy zijn in wervershove +Tot sy gaen in 't Hemels-Hof. +Anagramma. + +Hubertus van der Meer. +Is Wervershover eer, +En Bailly 's herten-lust: +Dus volght hy het beleydt +Van sijnen naem, welck seydt +Beraem hun Vrede-rust. +
+
+
+ + + + + +Wellekomst, Aen Iuffr. Geertruyde Maria Bailly. Gekomen tot Wervershoof den 5. September 1654. +
+ +Stemme: Roosemont die lagh gedoken. + WElkom, welkom waerde Vrouwe, +Puyck-cieraet van eer, en deught, +Die den Amstel stelt in rouwe, +Ende wervershoof in vreught: +Elck met ernst soo 't sijne pleght, +Dat het d' Hemel self beweeght. +2. d' Amstel weende uyt haer oogen, +Door 't verlies, van droefheydt seer: +d' Hemel schoot uyt mede-doogen +Oock terstondt haer tranen neer: +'t Scheen haer druppen letten woud, +Dat ghy niet vertrecken soud. +3. Maer doe sy ter ander zijde +Sagh de vreught van Wervershoof. + +Toond' haer d' Hemel weder blijde, +En sy 't swart Gordijn verschoof: +Ende sloot haer druypend' nat +Dicht in hooge water-vat. +4. Van dat 's morgens, Phebi paerden +Quamen voort met 't gulde Hooft, +Tot dat 's avondts, onder d' Aerde, +Thitis-plas dien Fackel dooft, +Saghmen in haer aengesicht. +Niet als lacchend' blinckend' licht. +5. Was ons u, o waerde Vrouwe, +Dat de Son dus vrolijck scheen. +d' Hemel zegen voort u trouwe, +Ende gun dat ghy gemeen +Met uw' lieven weder-paer, +Hier meught leven hondert-jaer. +
+
+
+ + +Sluyt-Veers. +Dit Veersje dient noch tot besluyt, +En daer mee is mijn Boeckjen uyt, +Is noch uw' sangh-lust niet voldaen, +Soo singh het weer van voeren aen. +
+
+ + +Register der Liedekens. +
+A. + + ACh gesalfde van den Heer.Pag. 30 + Als Saul, en david den vyant in't velt.41 + Als ick de Son verhoogen sie.184 + Als hem de Son begeeft.189 + Als ick den Herfst aenschou.194 + Als in koelt, de nacht komt overkleeden208 + Als van der meer op Eng'le-vleug'len vloog.232 +
+
+
+D. + + De wijsheydt die is uyt gevaren.11 + De soete mey17 +
+ + + Doe de Koningh dit aenhoorde.33 + David die nu had verstaen.36 + Doe de hoogen Godt op eerde.39 + Des Hemels licht verdween44 + Doe eertijdts Iohannes vernam.55 + Daer zijn geen saucen die soeter doen smaken72 + De valsche vrinde.80 + Daer is geen staet te noemen.89 + Draeght nu vry geen swarte rocken116 + Daer quam van eenen eter spijs.147 + Doe nu ons Vaderlandt.174 + De zielen die vry van het lijdende vleys.212 + Doe ick onlanghs de ruyge velden.225 + Dit is licht voor elck te lesen.229 +
+
+
+ +E. + + Een goedt verstant, een wijs beleyt.28 + Een mensch die hoord te weten.47 + Een Aexter was eens bloot en kael.57 + Een Aep die eenen vreemden lust.60 + Een oude Fabel, van lange wijl.78 + En roemt niet van u Landt, noch Stadt.96 + Een mensche van natuur.110 + Een moordenaer die altoos // Goddeloos.132 +
+
+
+G. + + Ghy dochters iongh en teer.22 + Geluck, ghy die de tijdt van strijdt.82 +
+ + + Gelijck een Vogel die daer henen.123 + Ghy die aen lust, en weeldt.155 + Gelijck Godt met sijn Wijnbergh dee.164 +
+
+
+H. + + Het is een wonder om aen te mercken.67 + Hef op, mijn Sangh-Goddin een Liedt.85 + Het eerste daghjen 't Nieuwe-Iaer.158 + Hef op u hert en handen.212 +
+
+
+I. + + Indienje eens willet letten.103 + Ick voel mijn Geest gedreven.119 + Ick was schier geweken.129 + Ick weet Heere, ghy sijt seecker.142 +
+
+
+ +K. + + Keerom, keerom, o Wereldts Kindt68 + 'k Wil de Liefste diemen siet.160 +
+
+
+L. + + Looft Godt alle ghy verloste.139 +
+
+
+M. + + Men siet de Min, en haren dwangh.20 + My dunckt dan eenen veugel.51 + Maer doe ons Vaderlandt.170 + Men hoort nu Hollandt klagen.181 +
+
+
+ +O. + + O soete Ieught, die in u jeughde zijt.8 + O groot geluck voor die 't geniet.14 + O soete jeught, weest niet hooveerdigh.150 + Om de Echt van dees gepaerde.237 +
+
+
+V. + + Vraeghje wie het meeste goedt.107 + Voor swellen die besloten zijn.136 +
+
+
+W. + + Wat is de jeught, en 't lieflijck bloosen.25 + Wilje wercken op goe voet.64 +
+ + + Wanneer als Godt sijn strengh gericht.75 + Wilje u vermaken.93 + Wilje weten wat de Neeringh.100 + Wie wel bemerckt de ordeningh.113 + Wie ist, die vry, met ongeboeyde ziele.26 + Wanneer de Son sijn stralen.197 + Wil yemandt in het kort.201 + Wanneer oprijst, de Sonne in den morgen.209 + Waeck op verstockte Nederlanden.220 + Welkom, welkom, waerde Vrouwe.240 +
+

 

+

FINIS.

+ + +

 

+

Tot Harlingen,

+

 

+

Gedruckt by Evert Idzes van Doorn. Ordinaris Boeck-drucker deser Stede, wonende op de Bree-Plaets 1671.

+

 

+
+
+
+ +
+
diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py new file mode 100644 index 000000000..c6fd83fbe --- /dev/null +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -0,0 +1,28 @@ +import pytest +import os + +from addcorpus.load_corpus import load_corpus + +here = os.path.abspath(os.path.dirname(__file__)) + +@pytest.fixture +def dbnl_corpus(settings): + settings.DBNL_DATA = os.path.join(here, 'data') + settings.CORPORA = { + 'dbnl': os.path.join(here, '..', 'dbnl.py') + } + return 'dbnl' + +expected_docs = [ + { + 'title': 'Het singende nachtegaeltje', + 'author': 'Cornelis Maertsz.' + } +] + +def test_dbnl_extraction(dbnl_corpus): + corpus = load_corpus(dbnl_corpus) + docs = corpus.documents() + + for actual, expected in zip(docs, expected_docs): + assert actual == expected From 2d921239b5aca31ed8249d68e2963058d2f9ee87 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 2 May 2023 15:25:17 +0200 Subject: [PATCH 024/262] extract title ID --- backend/corpora/dbnl/dbnl.py | 19 ++++++++++++++++--- .../dbnl/tests/test_dbnl_extraction.py | 5 +++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 6a1da0dc6..7299526a7 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -1,7 +1,8 @@ from datetime import datetime import os from django.conf import settings -from addcorpus.corpus import XMLCorpus +from addcorpus.corpus import XMLCorpus, Field +from addcorpus.extract import Metadata, XML class DBNL(XMLCorpus): title = 'DBNL' @@ -17,6 +18,18 @@ class DBNL(XMLCorpus): def sources(self, start = None, end = None): for filename in os.listdir(self.data_directory): - yield os.path.join(self.data_directory, filename), {} + id, *_ = filename.split('_') + metadata = {'id': id} + yield os.path.join(self.data_directory, filename), metadata - fields = [ ] + title_id = Field( + name='title_id', + display_name='Title ID', + display_type='text', + description='ID of the work', + extractor = Metadata('id') + ) + + fields = [ + title_id, + ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index c6fd83fbe..7ce2372c7 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -15,8 +15,9 @@ def dbnl_corpus(settings): expected_docs = [ { - 'title': 'Het singende nachtegaeltje', - 'author': 'Cornelis Maertsz.' + 'title_id': 'maer005sing01', + # 'title': 'Het singende nachtegaeltje', + # 'author': 'Cornelis Maertsz.' } ] From 0556dc3b1300d2c0fbb182011f8169a7232827bb Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 11:23:27 +0200 Subject: [PATCH 025/262] update language options based on dbnl --- backend/addcorpus/constants.py | 6 ++++-- backend/corpora/jewishinscriptions/jewishinscriptions.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/addcorpus/constants.py b/backend/addcorpus/constants.py index f3c858bbf..45e9c8ce7 100644 --- a/backend/addcorpus/constants.py +++ b/backend/addcorpus/constants.py @@ -13,17 +13,19 @@ ('en', 'English'), # stopword + stemming support ('fi', 'Finnish'), # stopword + stemming support ('fr', 'French'), # stopword + stemming support + ('fry', 'Frisian'), ('gd', 'Gaelic'), ('de', 'German'), # stopword + stemming support + ('nds', 'Low German'), ('grc', 'Ancient Greek'), ('el', 'Greek'), # stopword + stemming support - ('he', 'Hebrew'), # stopword support + ('heb', 'Hebrew'), # stopword support ('hu', 'Hungarian'), # stopword + stemming support ('ind', 'Indonesian'), # stopword + stemming support ('ga', 'Irish'), # stemming support ('it', 'Italian'), # stopword + stemming support ('kaz', 'Kazakh'), # stopword support - ('la', 'Latin'), + ('lat', 'Latin'), ('ne', 'Nepali'), # stopword support ('no', 'Norwegian'), # stopword + stemming supported for bokmål; the key for both is 'norwegian' ('nob', 'Norwegian (Bokmål)'), diff --git a/backend/corpora/jewishinscriptions/jewishinscriptions.py b/backend/corpora/jewishinscriptions/jewishinscriptions.py index 1f0e5c716..b0684f1c4 100644 --- a/backend/corpora/jewishinscriptions/jewishinscriptions.py +++ b/backend/corpora/jewishinscriptions/jewishinscriptions.py @@ -23,7 +23,7 @@ class JewishInscriptions(XMLCorpus): es_index = getattr(settings, 'JEWISH_INSCRIPTIONS_ES_INDEX', 'jewishinscriptions') image = 'jewish_inscriptions.jpg' visualize = [] - languages = ['he', 'la'] + languages = ['heb', 'lat'] category = 'inscription' # Data overrides from .common.XMLCorpus From 98284261d3c8602d73aa7619eb091268acfc7acd Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 2 May 2023 15:57:04 +0200 Subject: [PATCH 026/262] metadata extraction --- backend/corpora/dbnl/dbnl.py | 127 +++++++++++++++++- backend/corpora/dbnl/tests/data/titels_pd.csv | 11 ++ .../data/{ => xml_pd}/maer005sing01_01.xml | 0 .../dbnl/tests/test_dbnl_extraction.py | 27 +++- backend/corpora/dbnl/utils.py | 98 ++++++++++++++ 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 backend/corpora/dbnl/tests/data/titels_pd.csv rename backend/corpora/dbnl/tests/data/{ => xml_pd}/maer005sing01_01.xml (100%) create mode 100644 backend/corpora/dbnl/utils.py diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 7299526a7..d9ef46ed9 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -1,8 +1,36 @@ from datetime import datetime import os + from django.conf import settings from addcorpus.corpus import XMLCorpus, Field from addcorpus.extract import Metadata, XML +from corpora.dbnl.utils import * + +def author_extractor(field, extract=dict.get, join=list): + ''' + Create an extractor for author metadata. + + Input: + - field: the field of the author data + - extract(author, field): function to extract the value for each author, + based on the author dict and the field. Defaults to `dict.get`. + - join(values): function to join the extracted values for each author + ''' + + return Metadata( + 'auteurs', + transform=lambda authors: join(extract(author, field) for author in authors) + ) + + +def between_years(year, start_date, end_date): + if start_date and year < start_date.year: + return False + + if end_date and year > end_date.year: + return False + + return True class DBNL(XMLCorpus): title = 'DBNL' @@ -17,19 +45,108 @@ class DBNL(XMLCorpus): tag_entry = 'text' def sources(self, start = None, end = None): - for filename in os.listdir(self.data_directory): - id, *_ = filename.split('_') - metadata = {'id': id} - yield os.path.join(self.data_directory, filename), metadata + xml_dir = os.path.join(self.data_directory, 'xml_pd') + csv_path = os.path.join(self.data_directory, 'titels_pd.csv') + all_metadata = extract_metadata(csv_path) + + for filename in os.listdir(xml_dir): + if filename.endswith('.xml'): + id, *_ = filename.split('_') + metadata = {'id': id, **all_metadata[id]} + year = int(metadata['_jaar']) + + if between_years(year, start, end): + yield os.path.join(xml_dir, filename), metadata + + title_field = Field( + name='title', + display_name='Title', + display_type='text', + description='Title of the book', + extractor=Metadata('titel') + ) title_id = Field( name='title_id', display_name='Title ID', display_type='text', - description='ID of the work', + description='ID of the book', extractor = Metadata('id') ) + volumes = Field( + name='volumes', + extractor=Metadata('vols'), + ) + + # text version of the year, can include things like 'ca. 1500', '14e eeuw' + year_full = Field( + name='year_full', + extractor=Metadata('jaar') + ) + + # version of the year that is always a number + year_int = Field( + name='year', + extractor=Metadata('_jaar') + ) + + edition = Field( + name='edition', + extractor=Metadata('druk') + ) + + # ppn_o + # bibliotheek + # categorie + + author = Field( + name='author', + extractor=author_extractor( + ['voornaam', 'voorvoegsel', 'achternaam'], + extract=lambda author, keys: ' '.join(author[key] for key in keys if author[key]), + join=', '.join + ) + ) + + author_id = Field( + name='author_id', + extractor=author_extractor('pers_id',) + ) + + # jaar_geboren + # jaar_overlijden + # geb_datum + # overl_datum + # geb_plaats + # overl_plaats + # geb_plaats_code + # geb_land_code + # overl_plaats_code + # overl_land_code + # vrouw + + url = Field( + name='url', + extractor=Metadata('url') + ) + + url_txt = Field( + name = 'url_txt', + extractor=Metadata('text_url') + ) + + # genre + fields = [ + title_field, title_id, + volumes, + edition, + year_full, + year_int, + author, + author_id, + url, + url_txt, ] diff --git a/backend/corpora/dbnl/tests/data/titels_pd.csv b/backend/corpora/dbnl/tests/data/titels_pd.csv new file mode 100644 index 000000000..23f58d586 --- /dev/null +++ b/backend/corpora/dbnl/tests/data/titels_pd.csv @@ -0,0 +1,11 @@ +sep=| +ti_id|titel|vols|jaar|druk|ppn_o|bibliotheek|categorie|_jaar|pers_id|voornaam|voorvoegsel|achternaam|jaar_geboren|jaar_overlijden|geb_datum|overl_datum|geb_plaats|overl_plaats|geb_plaats_code|geb_land_code|overl_plaats_code|overl_land_code|vrouw|url|text_url|maand|genre| +"maer002alex01"|"Alexanders geesten"||"13de eeuw"|"handschrift"|||"1"|"1200"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002alex01_01"|||"poëzie"| +"maer002spie00"|"Spiegel historiael (5 delen)"||"ca. 1283-1325"|"handschrift"|||"1"|"1283"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002spie00_01"|||"poëzie"| +"maer002spie00"|"Spiegel historiael (5 delen)"||"ca. 1283-1325"|"handschrift"|||"1"|"1283"|"uten001"|"Philip"||"Utenbroecke"|"?(13de eeuw)"|"?(14de eeuw)"|||||||||"0"|"https://dbnl.org/tekst/maer002spie00_01"|||"poëzie"| +"maer002spie00"|"Spiegel historiael (5 delen)"||"ca. 1283-1325"|"handschrift"|||"1"|"1283"|"velt003"|"Lodewijk"|"van"|"Velthem"|"ca. 1270"|"na 1326"||||"Waldenrath"|||"walde001"||"0"|"https://dbnl.org/tekst/maer002spie00_01"|||"poëzie"| +"maer002spie02"|"Spiegel historiael. Eerste partie"||"ca. 1283-1296"|"handschrift"|||"1"|"1283"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002spie02_01"|||"poëzie"| +"maer002spie05"|"Spiegel historiael. Derde partie"||"ca. 1283-1296"|"handschrift"|||"1"|"1283"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002spie05_01"|||"poëzie"| +"maer002spie06"|"Spiegel historiael. Vierde partie"||"ca. 1283-1296"|"handschrift"|||"1"|"1283"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002spie06_01"|||"poëzie"| +"maer005sing01"|"Het singende nachtegaeltje"||"1671"|"1ste druk"|"393478793"|"denha004koni01"|"1"|"1671"|"maer005"|"Cornelis"||"Maertsz."|"?"|"na 1671"|||"Wervershoof"||"werve001"||||"0"|"https://dbnl.org/tekst/maer005sing01_01"|"https://dbnl.org/nieuws/text.php?id=maer005sing01"|"2012_10 "|"poëzie"| +"maer005stic01"|"Stichtelijcke gesangen"||"1661"|"1ste druk"|"393478807"|"leide001univ01"|"1"|"1661"|"maer005"|"Cornelis"||"Maertsz."|"?"|"na 1671"|||"Wervershoof"||"werve001"||||"0"|"https://dbnl.org/tekst/maer005stic01_01"|"https://dbnl.org/nieuws/text.php?id=maer005stic01"|"2013_02 "|"poëzie"| diff --git a/backend/corpora/dbnl/tests/data/maer005sing01_01.xml b/backend/corpora/dbnl/tests/data/xml_pd/maer005sing01_01.xml similarity index 100% rename from backend/corpora/dbnl/tests/data/maer005sing01_01.xml rename to backend/corpora/dbnl/tests/data/xml_pd/maer005sing01_01.xml diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 7ce2372c7..74158cebb 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -2,9 +2,24 @@ import os from addcorpus.load_corpus import load_corpus +from corpora.dbnl.utils import extract_metadata here = os.path.abspath(os.path.dirname(__file__)) +def test_metadata_extraction(): + csv_path = os.path.join(here, 'data', 'titels_pd.csv') + data = extract_metadata(csv_path) + assert len(data) == 7 + + item = data['maer005sing01'] + assert item['titel'] == 'Het singende nachtegaeltje' + assert len(item['auteurs']) == 1 + + multiple_authors = data['maer002spie00'] + assert multiple_authors['titel'] == 'Spiegel historiael (5 delen)' + assert len(multiple_authors['auteurs']) == 3 + + @pytest.fixture def dbnl_corpus(settings): settings.DBNL_DATA = os.path.join(here, 'data') @@ -13,11 +28,19 @@ def dbnl_corpus(settings): } return 'dbnl' + expected_docs = [ { 'title_id': 'maer005sing01', - # 'title': 'Het singende nachtegaeltje', - # 'author': 'Cornelis Maertsz.' + 'title': 'Het singende nachtegaeltje', + 'volumes': None, + 'edition': '1ste druk', + 'author_id': ['maer005'], + 'author': 'Cornelis Maertsz.', + 'url': 'https://dbnl.org/tekst/maer005sing01_01', + 'url_txt': 'https://dbnl.org/nieuws/text.php?id=maer005sing01', + 'year': '1671', + 'year_full': '1671', } ] diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py new file mode 100644 index 000000000..c81262b74 --- /dev/null +++ b/backend/corpora/dbnl/utils.py @@ -0,0 +1,98 @@ +import csv +from functools import reduce + +empty_to_none = lambda value : value if value != '' else None + +# titles may be included in multiple rows to give info about multiple authors +# the following fields describe the author and should be grouped into a list of dicts +author_fields = { + 'pers_id', + 'voornaam', + 'voorvoegsel', + 'achternaam', + 'jaar_geboren', + 'geb_datum', + 'geb_plaats', + 'geb_plaats_code', + 'geb_land_code', + 'jaar_overlijden', + 'overl_datum', + 'overl_land_code', + 'overl_plaats', + 'overl_plaats_code', + 'vrouw', +} + +# the following fields should be made into a list since they can have multiple values +# (but unlike with the author fields, no grouping of fields is necessary) +plural_fields = [ + 'genre' +] + +def formatted_items(row): + return ( + (key, empty_to_none(value)) + for key, value in row.items() + ) + +def extract_metadata(csv_path): + ''' + Extract all metadata from a CSV file. + + Returns the metdata per title ID + ''' + + with open(csv_path) as csv_file: + first_line = csv_file.readline() + _, sep = first_line.strip().split('=') + reader = csv.DictReader(csv_file, delimiter=sep) + + data = reduce(add_metadata_row, reader, {}) + + return data + +def add_metadata_row(data, row): + id = row['ti_id'] + + if id not in data: + data[id] = data_from_row(row) + else: + data[id] = update_data_with_row(data[id], row) + + return data + +def row_author_data(row): + return { + key: value + for key, value in formatted_items(row) + if key in author_fields + } + +def data_from_row(row): + author = row_author_data(row) + plurals = { + key: [empty_to_none(row[key])] + for key in plural_fields + } + rest = { + key: value + for key, value in formatted_items(row) + if key not in author_fields and key not in plural_fields + } + + return { + 'auteurs': [author], + **plurals, + **rest + } + +def update_data_with_row(data, row): + for key in plural_fields: + if row[key] not in data[key]: + data[key].append(empty_to_none(row[key])) + + author = row_author_data(row) + if author not in data['auteurs']: + data['auteurs'].append(author) + + return data From 41cf440db92aaae3e32aeb5d9a5f51b4f6b07493 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 13:02:50 +0200 Subject: [PATCH 027/262] allow more complex tag search in XML extractor --- backend/addcorpus/corpus.py | 43 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index b38e0005a..cf1cc8728 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -357,10 +357,10 @@ def source2dicts(self, source): required_fields = [ field.name for field in self.fields if field.required] # Extract fields from the soup - tag = self.get_entry_tag(metadata) + tag = self.get_tag_requirements(self.tag_entry, metadata) bowl = self.bowl_from_soup(soup, metadata=metadata) if bowl: - spoonfuls = bowl.find_all(tag) if tag else [bowl] + spoonfuls = bowl.find_all(**tag) if tag else [bowl] for spoon in spoonfuls: regular_field_dict = {field.name: field.extractor.apply( # The extractor is put to work by simply throwing at it @@ -387,19 +387,32 @@ def source2dicts(self, source): logger.warning( 'Top-level tag not found in `{}`'.format(filename)) - def get_entry_tag(self, metadata): - if type(self.tag_entry) == str: - return self.tag_entry - elif self.tag_entry is None: - return None + def get_tag_requirements(self, specification, metadata): + ''' + Get the requirements for a tag given the specification. + + The specification can be: + - None + - A string with the name of the tag + - A dict with the named arguments to soup.find() / soup.find_all() + - A callable that takes the document metadata as input and outputs one of the above. + + Output is either None or a dict with the arguments for soup.find() / soup.find_all() + ''' + + if callable(specification): + condition = specification(metadata) else: - return self.tag_entry(metadata) + condition = specification - def get_toplevel_tag(self, metadata): - if type(self.tag_toplevel) == str: - return self.tag_toplevel + if condition is None: + return None + elif type(condition) == str: + return {'name': condition} + elif type(condition) == dict: + return condition else: - return self.tag_toplevel(metadata) + raise TypeError('Tag must be a string or dict') def external_source2dict(self, soup, external_fields, metadata): ''' @@ -453,11 +466,9 @@ def bowl_from_soup(self, soup, toplevel_tag=None, entry_tag=None, metadata = {}) If no such tag is present, it contains the entire soup. ''' if toplevel_tag == None: - toplevel_tag = self.get_toplevel_tag(metadata) - if entry_tag == None: - entry_tag = self.get_entry_tag(metadata) + toplevel_tag = self.get_tag_requirements(self.tag_toplevel, metadata) - return soup.find(toplevel_tag) if toplevel_tag else soup + return soup.find(**toplevel_tag) if toplevel_tag else soup def metadata_from_xml(self, filename, tags): ''' From 694a6d7c24e3acfa447bfe4231b453d081e79cb8 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 13:58:54 +0200 Subject: [PATCH 028/262] extract content --- backend/addcorpus/corpus.py | 16 +++++++- backend/corpora/dbnl/dbnl.py | 41 +++++++++++++++++-- .../dbnl/tests/test_dbnl_extraction.py | 12 +++++- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index cf1cc8728..dbeb3967b 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -302,13 +302,25 @@ class XMLCorpus(Corpus): @property def tag_toplevel(self): ''' - The top-level tag in the source documents. Either a string or a function that maps metadata to a string. + The top-level tag in the source documents. + + Can be: + - None + - A string with the name of the tag + - A dictionary that gives the named arguments to soup.find_all() + - A bound method that takes the metadata of the document as input and outputs one of the above. ''' @property def tag_entry(self): ''' - The tag that corresponds to a single document entry. Either a string or a function that maps metadata to a string. + The tag that corresponds to a single document entry. + + Can be: + - None + - A string with the name of the tag + - A dictionary that gives the named arguments to soup.find_all() + - A bound method that takes the metadata of the document as input and outputs one of the above. ''' def source2dicts(self, source): diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index d9ef46ed9..b5f44ac60 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -1,5 +1,6 @@ from datetime import datetime import os +from bs4 import BeautifulSoup from django.conf import settings from addcorpus.corpus import XMLCorpus, Field @@ -32,6 +33,21 @@ def between_years(year, start_date, end_date): return True +def find_entry_level(xml_path): + with open(xml_path) as xml_file: + soup = BeautifulSoup(xml_file, 'lxml-xml') + + # potential levels of documents, in order of preference + levels = [ + # { 'name': 'div', 'attrs': {'type': 'section'} }, + { 'name': 'div', 'attrs': {'type': 'chapter'} }, + { 'name': 'text' } + ] + + level = next(level for level in levels if soup.find(**level)) + + return level + class DBNL(XMLCorpus): title = 'DBNL' description = 'Digitale Bibliotheek voor de Nederlandse letteren' @@ -42,7 +58,9 @@ class DBNL(XMLCorpus): image = 'dbnl-logo.jpeg' tag_toplevel = 'TEI.2' - tag_entry = 'text' + + def tag_entry(self, metadata): + return metadata['xml_entry_level'] def sources(self, start = None, end = None): xml_dir = os.path.join(self.data_directory, 'xml_pd') @@ -52,11 +70,18 @@ def sources(self, start = None, end = None): for filename in os.listdir(xml_dir): if filename.endswith('.xml'): id, *_ = filename.split('_') - metadata = {'id': id, **all_metadata[id]} + path = os.path.join(xml_dir, filename) + entry_level = find_entry_level(path) + metadata = { + 'id': id, + 'xml_entry_level': entry_level, + **all_metadata[id] + } + year = int(metadata['_jaar']) if between_years(year, start, end): - yield os.path.join(xml_dir, filename), metadata + yield path, metadata title_field = Field( name='title', @@ -138,6 +163,15 @@ def sources(self, start = None, end = None): # genre + content = Field( + name='content', + extractor=XML( + tag='p', + multiple=True, + flatten=True, + ) + ) + fields = [ title_field, title_id, @@ -149,4 +183,5 @@ def sources(self, start = None, end = None): author_id, url, url_txt, + content, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 74158cebb..0d98f12b5 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -28,7 +28,6 @@ def dbnl_corpus(settings): } return 'dbnl' - expected_docs = [ { 'title_id': 'maer005sing01', @@ -41,12 +40,21 @@ def dbnl_corpus(settings): 'url_txt': 'https://dbnl.org/nieuws/text.php?id=maer005sing01', 'year': '1671', 'year_full': '1671', + 'content': '\n'.join([ + 'Het singende Nachtegaeltje', + 'Quelende soetelijck, tot stichtelijck vermaeck voor de Christelijck Ieught.', + 'Door.', + 'Cornelis Maertsz. tot Wervers hoof.', + '\'t Amsterdam Voor Michiel de Groot, Boek-Verkooper op den Nieuwen Dijck, 1671.', + ]) } ] def test_dbnl_extraction(dbnl_corpus): corpus = load_corpus(dbnl_corpus) - docs = corpus.documents() + docs = list(corpus.documents()) + + assert len(docs) == 70 for actual, expected in zip(docs, expected_docs): assert actual == expected From e4a60f4ded6a707a4670f3984210215eecd6b21d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 15:08:16 +0200 Subject: [PATCH 029/262] more fields --- backend/corpora/dbnl/dbnl.py | 64 ++++++++++++++----- .../dbnl/tests/test_dbnl_extraction.py | 13 +++- backend/corpora/dbnl/utils.py | 8 +++ 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index b5f44ac60..5775ba618 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -7,7 +7,7 @@ from addcorpus.extract import Metadata, XML from corpora.dbnl.utils import * -def author_extractor(field, extract=dict.get, join=list): +def author_extractor(field, extract=dict.get, join=', '.join): ''' Create an extractor for author metadata. @@ -15,7 +15,7 @@ def author_extractor(field, extract=dict.get, join=list): - field: the field of the author data - extract(author, field): function to extract the value for each author, based on the author dict and the field. Defaults to `dict.get`. - - join(values): function to join the extracted values for each author + - join(values): function to join the formatted values for each author ''' return Metadata( @@ -130,26 +130,46 @@ def sources(self, start = None, end = None): extractor=author_extractor( ['voornaam', 'voorvoegsel', 'achternaam'], extract=lambda author, keys: ' '.join(author[key] for key in keys if author[key]), - join=', '.join ) ) author_id = Field( name='author_id', - extractor=author_extractor('pers_id',) + extractor=author_extractor('pers_id',), ) - # jaar_geboren - # jaar_overlijden - # geb_datum - # overl_datum - # geb_plaats - # overl_plaats - # geb_plaats_code - # geb_land_code - # overl_plaats_code - # overl_land_code - # vrouw + author_year_of_birth = Field( + name='author_year_of_birth', + extractor=author_extractor('jaar_geboren'), + ) + + author_year_of_death = Field( + name='author_year_of_death', + extractor=author_extractor('jaar_overlijden'), + ) + + # these fields are given as proper dates in geb_datum / overl_datum + # but implementing them as date fields requires support for multiple values + + author_place_of_birth = Field( + name='author_place_of_birth', + extractor=author_extractor('geb_plaats'), + ) + + author_place_of_death = Field( + name='author_place_of_death', + extractor=author_extractor('overl_plaats') + ) + + # gender is coded as a binary value (∈ ['1', '0']) + # converted to a string to be more comparable with other corpora + author_gender = Field( + name='author_gender', + extractor=author_extractor( + 'vrouw', + # format=lambda value: {'0': 'man', '1': 'vrouw'}.get(value, None), + ) + ) url = Field( name='url', @@ -161,7 +181,13 @@ def sources(self, start = None, end = None): extractor=Metadata('text_url') ) - # genre + genre = Field( + name='genre', + extractor=Metadata( + 'genre', + transform=', '.join, + ) + ) content = Field( name='content', @@ -181,7 +207,13 @@ def sources(self, start = None, end = None): year_int, author, author_id, + author_year_of_birth, + author_place_of_birth, + author_year_of_death, + author_place_of_death, + # author_gender, url, url_txt, + # genre, content, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 0d98f12b5..d379dbb6b 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -2,10 +2,13 @@ import os from addcorpus.load_corpus import load_corpus -from corpora.dbnl.utils import extract_metadata +from corpora.dbnl.utils import extract_metadata, compose here = os.path.abspath(os.path.dirname(__file__)) +def test_compose(): + assert compose(str.upper, ' '.join)(['a', 'b']) == 'A B' + def test_metadata_extraction(): csv_path = os.path.join(here, 'data', 'titels_pd.csv') data = extract_metadata(csv_path) @@ -34,12 +37,18 @@ def dbnl_corpus(settings): 'title': 'Het singende nachtegaeltje', 'volumes': None, 'edition': '1ste druk', - 'author_id': ['maer005'], + 'author_id': 'maer005', 'author': 'Cornelis Maertsz.', + 'author_year_of_birth': '?', + 'author_place_of_birth': 'Wervershoof', + 'author_year_of_death': 'na 1671', + 'author_place_of_death': None, + # 'author_gender': 'man', 'url': 'https://dbnl.org/tekst/maer005sing01_01', 'url_txt': 'https://dbnl.org/nieuws/text.php?id=maer005sing01', 'year': '1671', 'year_full': '1671', + # 'genre': 'poëzie', 'content': '\n'.join([ 'Het singende Nachtegaeltje', 'Quelende soetelijck, tot stichtelijck vermaeck voor de Christelijck Ieught.', diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index c81262b74..ae3d06f25 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -96,3 +96,11 @@ def update_data_with_row(data, row): data['auteurs'].append(author) return data + +def compose(*functions): + ''' + Given a list of functions, returns a new function that is the composition of all + + e.g. compose(str.upper, ' '.join)(['a', 'b']) == 'A B' + ''' + return lambda y: reduce(lambda x, func: func(x), reversed(functions), y) From 8ef33764083c89180fff0755995db447ebabff5f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 15:09:16 +0200 Subject: [PATCH 030/262] move functions to utils file --- backend/corpora/dbnl/utils.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index ae3d06f25..5696bbc09 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -1,5 +1,8 @@ import csv from functools import reduce +from bs4 import BeautifulSoup +from addcorpus.extract import Metadata + empty_to_none = lambda value : value if value != '' else None @@ -104,3 +107,45 @@ def compose(*functions): e.g. compose(str.upper, ' '.join)(['a', 'b']) == 'A B' ''' return lambda y: reduce(lambda x, func: func(x), reversed(functions), y) + + +def author_extractor(field, extract=dict.get, join=', '.join): + ''' + Create an extractor for author metadata. + + Input: + - field: the field of the author data + - extract(author, field): function to extract the value for each author, + based on the author dict and the field. Defaults to `dict.get`. + - join(values): function to join the formatted values for each author + ''' + + return Metadata( + 'auteurs', + transform=lambda authors: join(extract(author, field) for author in authors) + ) + + +def between_years(year, start_date, end_date): + if start_date and year < start_date.year: + return False + + if end_date and year > end_date.year: + return False + + return True + +def find_entry_level(xml_path): + with open(xml_path) as xml_file: + soup = BeautifulSoup(xml_file, 'lxml-xml') + + # potential levels of documents, in order of preference + levels = [ + # { 'name': 'div', 'attrs': {'type': 'section'} }, + { 'name': 'div', 'attrs': {'type': 'chapter'} }, + { 'name': 'text' } + ] + + level = next(level for level in levels if soup.find(**level)) + + return level From 9d3fe17bc3e4a77af23164e6e464c194cb8c43f6 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 15:12:09 +0200 Subject: [PATCH 031/262] alternative implementation of author metadata --- backend/addcorpus/corpus.py | 3 + backend/addcorpus/extract.py | 13 +++ backend/corpora/dbnl/dbnl.py | 85 ++++++------------- .../dbnl/tests/test_dbnl_extraction.py | 4 +- backend/corpora/dbnl/utils.py | 14 ++- 5 files changed, 53 insertions(+), 66 deletions(-) diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index dbeb3967b..b9e9a5e26 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -338,6 +338,7 @@ def source2dicts(self, source): extract.Constant, extract.ExternalFile, extract.Backup, + extract.Pass, )): raise RuntimeError( "Specified extractor method cannot be used with an XML corpus") @@ -552,6 +553,7 @@ def source2dicts(self, source): extract.Metadata, extract.Constant, extract.Backup, + extract.Pass, )): raise RuntimeError( "Specified extractor method cannot be used with an HTML corpus") @@ -635,6 +637,7 @@ def source2dicts(self, source): extract.Constant, extract.Backup, extract.Metadata, + extract.Pass, )): raise RuntimeError( "Specified extractor method cannot be used with a CSV corpus") diff --git a/backend/addcorpus/extract.py b/backend/addcorpus/extract.py index ca51db37f..3518c7a90 100644 --- a/backend/addcorpus/extract.py +++ b/backend/addcorpus/extract.py @@ -125,6 +125,19 @@ def __init__(self, key, *nargs, **kwargs): def _apply(self, metadata, *nargs, **kwargs): return metadata.get(self.key) +class Pass(Extractor): + ''' + An extractor that just passes the value of another extractor. + + Useful if you want to stack multiple `transform` arguments + ''' + + def __init__(self, extractor, *nargs, **kwargs): + self.extractor = extractor + super().__init__(**kwargs) + + def _apply(self, *nargs, **kwargs): + return self.extractor.apply(*nargs, **kwargs) class XML(Extractor): ''' diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 5775ba618..3014509e8 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -4,50 +4,9 @@ from django.conf import settings from addcorpus.corpus import XMLCorpus, Field -from addcorpus.extract import Metadata, XML +from addcorpus.extract import Metadata, XML, Pass, Constant from corpora.dbnl.utils import * -def author_extractor(field, extract=dict.get, join=', '.join): - ''' - Create an extractor for author metadata. - - Input: - - field: the field of the author data - - extract(author, field): function to extract the value for each author, - based on the author dict and the field. Defaults to `dict.get`. - - join(values): function to join the formatted values for each author - ''' - - return Metadata( - 'auteurs', - transform=lambda authors: join(extract(author, field) for author in authors) - ) - - -def between_years(year, start_date, end_date): - if start_date and year < start_date.year: - return False - - if end_date and year > end_date.year: - return False - - return True - -def find_entry_level(xml_path): - with open(xml_path) as xml_file: - soup = BeautifulSoup(xml_file, 'lxml-xml') - - # potential levels of documents, in order of preference - levels = [ - # { 'name': 'div', 'attrs': {'type': 'section'} }, - { 'name': 'div', 'attrs': {'type': 'chapter'} }, - { 'name': 'text' } - ] - - level = next(level for level in levels if soup.find(**level)) - - return level - class DBNL(XMLCorpus): title = 'DBNL' description = 'Digitale Bibliotheek voor de Nederlandse letteren' @@ -127,47 +86,56 @@ def sources(self, start = None, end = None): author = Field( name='author', - extractor=author_extractor( - ['voornaam', 'voorvoegsel', 'achternaam'], - extract=lambda author, keys: ' '.join(author[key] for key in keys if author[key]), + extractor=join_extracted( + Combined( + author_extractor('voornaam'), + author_extractor('voorvoegsel'), + author_extractor('achternaam'), + transform=lambda values: [format_name(parts) for parts in zip(*values)] + ) ) ) author_id = Field( name='author_id', - extractor=author_extractor('pers_id',), + extractor=author_single_value_extractor('pers_id') ) author_year_of_birth = Field( name='author_year_of_birth', - extractor=author_extractor('jaar_geboren'), + extractor=author_single_value_extractor('jaar_geboren') ) author_year_of_death = Field( name='author_year_of_death', - extractor=author_extractor('jaar_overlijden'), + extractor=author_single_value_extractor('jaar_overlijden'), ) - # these fields are given as proper dates in geb_datum / overl_datum + # the above fields are also given as proper dates in geb_datum / overl_datum # but implementing them as date fields requires support for multiple values author_place_of_birth = Field( name='author_place_of_birth', - extractor=author_extractor('geb_plaats'), + extractor=author_single_value_extractor('geb_plaats'), ) author_place_of_death = Field( name='author_place_of_death', - extractor=author_extractor('overl_plaats') + extractor=author_single_value_extractor('overl_plaats') ) # gender is coded as a binary value (∈ ['1', '0']) # converted to a string to be more comparable with other corpora author_gender = Field( name='author_gender', - extractor=author_extractor( - 'vrouw', - # format=lambda value: {'0': 'man', '1': 'vrouw'}.get(value, None), + extractor=join_extracted( + Pass( + author_extractor('vrouw',), + transform=lambda values: map( + lambda value: {'0': 'man', '1': 'vrouw'}.get(value, None), + values + ), + ) ) ) @@ -183,10 +151,7 @@ def sources(self, start = None, end = None): genre = Field( name='genre', - extractor=Metadata( - 'genre', - transform=', '.join, - ) + extractor=join_extracted(Metadata('genre')) ) content = Field( @@ -211,9 +176,9 @@ def sources(self, start = None, end = None): author_place_of_birth, author_year_of_death, author_place_of_death, - # author_gender, + author_gender, url, url_txt, - # genre, + genre, content, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index d379dbb6b..d89bde7d2 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -43,12 +43,12 @@ def dbnl_corpus(settings): 'author_place_of_birth': 'Wervershoof', 'author_year_of_death': 'na 1671', 'author_place_of_death': None, - # 'author_gender': 'man', + 'author_gender': 'man', 'url': 'https://dbnl.org/tekst/maer005sing01_01', 'url_txt': 'https://dbnl.org/nieuws/text.php?id=maer005sing01', 'year': '1671', 'year_full': '1671', - # 'genre': 'poëzie', + 'genre': 'poëzie', 'content': '\n'.join([ 'Het singende Nachtegaeltje', 'Quelende soetelijck, tot stichtelijck vermaeck voor de Christelijck Ieught.', diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 5696bbc09..410f900d4 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -1,7 +1,7 @@ import csv from functools import reduce from bs4 import BeautifulSoup -from addcorpus.extract import Metadata +from addcorpus.extract import Metadata, Extractor, Combined, Pass empty_to_none = lambda value : value if value != '' else None @@ -109,7 +109,7 @@ def compose(*functions): return lambda y: reduce(lambda x, func: func(x), reversed(functions), y) -def author_extractor(field, extract=dict.get, join=', '.join): +def author_extractor(field): ''' Create an extractor for author metadata. @@ -117,14 +117,17 @@ def author_extractor(field, extract=dict.get, join=', '.join): - field: the field of the author data - extract(author, field): function to extract the value for each author, based on the author dict and the field. Defaults to `dict.get`. - - join(values): function to join the formatted values for each author ''' return Metadata( 'auteurs', - transform=lambda authors: join(extract(author, field) for author in authors) + transform=lambda authors: [author.get(field) for author in authors] ) +def join_extracted(extractor): + return Pass(extractor, transform=', '.join) + +author_single_value_extractor = compose(join_extracted, author_extractor) def between_years(year, start_date, end_date): if start_date and year < start_date.year: @@ -149,3 +152,6 @@ def find_entry_level(xml_path): level = next(level for level in levels if soup.find(**level)) return level + + +format_name = lambda parts: ' '.join(filter(None, parts)) From d6e39b5b46987f67fbe88737082c4f53d0198fc8 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 16:28:30 +0200 Subject: [PATCH 032/262] fancier joining function --- .../dbnl/tests/test_dbnl_extraction.py | 2 +- backend/corpora/dbnl/utils.py | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index d89bde7d2..c6793ae86 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -39,7 +39,7 @@ def dbnl_corpus(settings): 'edition': '1ste druk', 'author_id': 'maer005', 'author': 'Cornelis Maertsz.', - 'author_year_of_birth': '?', + 'author_year_of_birth': None, 'author_place_of_birth': 'Wervershoof', 'author_year_of_death': 'na 1671', 'author_place_of_death': None, diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 410f900d4..c23d58f02 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -124,8 +124,27 @@ def author_extractor(field): transform=lambda authors: [author.get(field) for author in authors] ) +def join_values(values): + ''' + Join extracted values into a string with proper handling of None values. + + This is intend to be used on an iterable of strings or None. + + - If all values are '', None, or '?', return None + - If some values are non-empty strings, convert falsy values to '?' and join + them into a single string. + ''' + + formatted = [value or '?' for value in values] + if any(value != '?' for value in formatted): + return ', '.join(formatted) + def join_extracted(extractor): - return Pass(extractor, transform=', '.join) + ''' + Apply an extractor that outputs an iterable, and return the results + in a comma-joined string. + ''' + return Pass(extractor, transform=join_values) author_single_value_extractor = compose(join_extracted, author_extractor) From 7a8f14aad60482b9b6f68be353ef0a68192d4a0f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 16:33:04 +0200 Subject: [PATCH 033/262] code cleanup --- backend/corpora/dbnl/dbnl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 3014509e8..380e38c7c 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -1,10 +1,9 @@ from datetime import datetime import os -from bs4 import BeautifulSoup from django.conf import settings from addcorpus.corpus import XMLCorpus, Field -from addcorpus.extract import Metadata, XML, Pass, Constant +from addcorpus.extract import Metadata, XML, Pass from corpora.dbnl.utils import * class DBNL(XMLCorpus): @@ -80,10 +79,6 @@ def sources(self, start = None, end = None): extractor=Metadata('druk') ) - # ppn_o - # bibliotheek - # categorie - author = Field( name='author', extractor=join_extracted( @@ -163,6 +158,11 @@ def sources(self, start = None, end = None): ) ) + # TODO: + # page start + # page stop + # language + fields = [ title_field, title_id, From ee8200e617f6a091fc404dd91a4e5c506b1a2993 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 16:38:39 +0200 Subject: [PATCH 034/262] extract language data --- backend/corpora/dbnl/dbnl.py | 22 ++++++++++++++++++- .../dbnl/tests/test_dbnl_extraction.py | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 380e38c7c..d7390cca9 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -149,6 +149,25 @@ def sources(self, start = None, end = None): extractor=join_extracted(Metadata('genre')) ) + language = Field( + name='language', + extractor=XML( + 'language', + toplevel=True, + recursive=True, + ) + ) + + language_code = Field( + name='language_code', + extractor=XML( + 'language', + attribute='id', + toplevel=True, + recursive=True, + ) + ) + content = Field( name='content', extractor=XML( @@ -161,7 +180,6 @@ def sources(self, start = None, end = None): # TODO: # page start # page stop - # language fields = [ title_field, @@ -180,5 +198,7 @@ def sources(self, start = None, end = None): url, url_txt, genre, + language, + language_code, content, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index c6793ae86..6537ebc31 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -49,6 +49,8 @@ def dbnl_corpus(settings): 'year': '1671', 'year_full': '1671', 'genre': 'poëzie', + 'language': 'Nederlands', + 'language_code': 'nl', 'content': '\n'.join([ 'Het singende Nachtegaeltje', 'Quelende soetelijck, tot stichtelijck vermaeck voor de Christelijck Ieught.', From 555aa99cef76a629c5bad2811900c63d7b8c7175 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 16:50:44 +0200 Subject: [PATCH 035/262] add order extractor --- backend/addcorpus/corpus.py | 49 ++++++------------- backend/addcorpus/extract.py | 4 ++ backend/corpora/dbnl/dbnl.py | 10 ++-- .../dbnl/tests/test_dbnl_extraction.py | 3 +- documentation/Defining-corpus-fields.md | 3 +- 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index b9e9a5e26..31f75720b 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -330,15 +330,8 @@ def source2dicts(self, source): ''' # Make sure that extractors are sensible for field in self.fields: - if not isinstance(field.extractor, ( - extract.Choice, - extract.Combined, - extract.XML, - extract.Metadata, - extract.Constant, - extract.ExternalFile, - extract.Backup, - extract.Pass, + if isinstance(field.extractor, ( + extract.HTML, extract.CSV, )): raise RuntimeError( "Specified extractor method cannot be used with an XML corpus") @@ -374,13 +367,14 @@ def source2dicts(self, source): bowl = self.bowl_from_soup(soup, metadata=metadata) if bowl: spoonfuls = bowl.find_all(**tag) if tag else [bowl] - for spoon in spoonfuls: + for i, spoon in enumerate(spoonfuls): regular_field_dict = {field.name: field.extractor.apply( # The extractor is put to work by simply throwing at it # any and all information it might need soup_top=bowl, soup_entry=spoon, - metadata=metadata + metadata=metadata, + index=i, ) for field in regular_fields if field.indexed} external_dict = {} if external_fields: @@ -546,14 +540,8 @@ def source2dicts(self, source): # Make sure that extractors are sensible for field in self.fields: - if not isinstance(field.extractor, ( - extract.Choice, - extract.Combined, - extract.HTML, - extract.Metadata, - extract.Constant, - extract.Backup, - extract.Pass, + if isinstance(field.extractor, ( + extract.XML, extract.CSV, )): raise RuntimeError( "Specified extractor method cannot be used with an HTML corpus") @@ -575,7 +563,7 @@ def source2dicts(self, source): # if there is a entry level tag, with html this is not always the case if bowl and tag: # Note that this is non-recursive: will only find direct descendants of the top-level tag - for spoon in bowl.find_all(tag): + for i, spoon in enumerate(bowl.find_all(tag)): # yield yield { field.name: field.extractor.apply( @@ -583,7 +571,8 @@ def source2dicts(self, source): # any and all information it might need soup_top=bowl, soup_entry=spoon, - metadata=metadata + metadata=metadata, + index=i ) for field in self.fields if field.indexed } else: @@ -594,7 +583,7 @@ def source2dicts(self, source): # any and all information it might need soup_top='', soup_entry=soup, - metadata=metadata + metadata=metadata, ) for field in self.fields if field.indexed } @@ -631,13 +620,7 @@ def source2dicts(self, source): csv.field_size_limit(sys.maxsize) for field in self.fields: if not isinstance(field.extractor, ( - extract.Choice, - extract.Combined, - extract.CSV, - extract.Constant, - extract.Backup, - extract.Metadata, - extract.Pass, + extract.HTML, extract.XML )): raise RuntimeError( "Specified extractor method cannot be used with a CSV corpus") @@ -655,7 +638,7 @@ def source2dicts(self, source): reader = csv.DictReader(f, delimiter=self.delimiter) document_id = None rows = [] - for row in reader: + for i, row in enumerate(reader): is_new_document = True if self.required_field and not row.get(self.required_field): # skip row if required_field is empty @@ -670,19 +653,19 @@ def source2dicts(self, source): document_id = identifier if is_new_document and rows: - yield self.document_from_rows(rows, metadata) + yield self.document_from_rows(rows, metadata, i) rows = [row] else: rows.append(row) yield self.document_from_rows(rows, metadata) - def document_from_rows(self, rows, metadata): + def document_from_rows(self, rows, metadata, row_index): doc = { field.name: field.extractor.apply( # The extractor is put to work by simply throwing at it # any and all information it might need - rows=rows, metadata = metadata + rows=rows, metadata = metadata, index=row_index ) for field in self.fields if field.indexed } diff --git a/backend/addcorpus/extract.py b/backend/addcorpus/extract.py index 3518c7a90..9e83fa479 100644 --- a/backend/addcorpus/extract.py +++ b/backend/addcorpus/extract.py @@ -139,6 +139,10 @@ def __init__(self, extractor, *nargs, **kwargs): def _apply(self, *nargs, **kwargs): return self.extractor.apply(*nargs, **kwargs) +class Index(Extractor): + def _apply(self, index=None, *nargs, **kwargs): + return index + class XML(Extractor): ''' This extractor extracts attributes or contents from a BeautifulSoup node. diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index d7390cca9..810cd9605 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -3,7 +3,7 @@ from django.conf import settings from addcorpus.corpus import XMLCorpus, Field -from addcorpus.extract import Metadata, XML, Pass +from addcorpus.extract import Metadata, XML, Pass, Index from corpora.dbnl.utils import * class DBNL(XMLCorpus): @@ -177,9 +177,10 @@ def sources(self, start = None, end = None): ) ) - # TODO: - # page start - # page stop + order_in_book = Field( + name='order_in_book', + extractor=Index(), + ) fields = [ title_field, @@ -201,4 +202,5 @@ def sources(self, start = None, end = None): language, language_code, content, + order_in_book, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 6537ebc31..4b90c0de6 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -57,7 +57,8 @@ def dbnl_corpus(settings): 'Door.', 'Cornelis Maertsz. tot Wervers hoof.', '\'t Amsterdam Voor Michiel de Groot, Boek-Verkooper op den Nieuwen Dijck, 1671.', - ]) + ]), + 'order_in_book': 0, } ] diff --git a/documentation/Defining-corpus-fields.md b/documentation/Defining-corpus-fields.md index 0d2e722fd..063f3e0f7 100644 --- a/documentation/Defining-corpus-fields.md +++ b/documentation/Defining-corpus-fields.md @@ -9,7 +9,8 @@ Various classes are defined in `backend/addcorpus/extract.py`. - The extractors `XML`, `HTML` and `CSV` are intended to extract values from the document type of your corpus. Naturally, `XML` is only available for `XMLCorpus`, et cetera. All other extractors are available for all corpora. - The `Metadata` extractor is used to collect any information that you passed on during file discovery, such as information based on the file path. - The `Constant` extractor can be used to define a constant value. -- The `Choice` and `Combined`, and `Backup` extractors can be used to combine multiple extractors. +- The `Index` extractor gives you the index of that document within the file. +- The `Choice` and `Combined`, `Backup`, and `Pass` extractors can be used to combine multiple extractors. A field can have the property `required = True`, which means the document will not be added to the index if the extracted value for this field is falsy. From bbcc5b57cec8d6e78bd6abbdd347443c0055b485 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 17:15:20 +0200 Subject: [PATCH 036/262] fill in fluff for fields --- backend/addcorpus/es_mappings.py | 5 + backend/corpora/dbnl/dbnl.py | 118 ++++++++++++++++++++---- documentation/Defining-corpus-fields.md | 4 +- 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/backend/addcorpus/es_mappings.py b/backend/addcorpus/es_mappings.py index 735325ea8..d8f126434 100644 --- a/backend/addcorpus/es_mappings.py +++ b/backend/addcorpus/es_mappings.py @@ -67,3 +67,8 @@ def date_mapping(format='yyyy-MM-dd'): 'type': 'date', 'format': format } + +def int_mapping(): + return { + 'type': 'integer' + } diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 810cd9605..f0d901b82 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -5,6 +5,8 @@ from addcorpus.corpus import XMLCorpus, Field from addcorpus.extract import Metadata, XML, Pass, Index from corpora.dbnl.utils import * +from addcorpus.es_mappings import * +from addcorpus.filters import RangeFilter, MultipleChoiceFilter class DBNL(XMLCorpus): title = 'DBNL' @@ -44,43 +46,69 @@ def sources(self, start = None, end = None): title_field = Field( name='title', display_name='Title', - display_type='text', description='Title of the book', - extractor=Metadata('titel') + results_overview=True, + search_field_core=True, + csv_core=True, + extractor=Metadata('titel'), + es_mapping=text_mapping(), + visualizations=['wordcloud'] ) title_id = Field( name='title_id', display_name='Title ID', - display_type='text', description='ID of the book', - extractor = Metadata('id') + extractor = Metadata('id'), + es_mapping=keyword_mapping() ) volumes = Field( name='volumes', + display_name='Volumes', + description='Number of volumes in which this book was published', extractor=Metadata('vols'), + es_mapping=text_mapping(), ) # text version of the year, can include things like 'ca. 1500', '14e eeuw' year_full = Field( name='year_full', - extractor=Metadata('jaar') + display_name='Publication year', + description='Year of publication in text format. May describe a range.', + results_overview=True, + csv_core=True, + extractor=Metadata('jaar'), + es_mapping=text_mapping(), ) # version of the year that is always a number year_int = Field( name='year', - extractor=Metadata('_jaar') + display_name='Publication year (est.)', + description='Year of publication as a number. May not be exact.', + extractor=Metadata('_jaar'), + es_mapping=int_mapping(), + search_filter=RangeFilter(lower=1200, upper=2020), + visualizations=['resultscount', 'termfrequency'], + sortable=True, ) edition = Field( name='edition', - extractor=Metadata('druk') + display_name='Edition', + description='Edition of the book', + extractor=Metadata('druk'), + es_mapping=text_mapping(), ) author = Field( name='author', + display_name='Author', + description='Name(s) of the author(s)', + results_overview=True, + search_field_core=True, + csv_core=True, extractor=join_extracted( Combined( author_extractor('voornaam'), @@ -88,22 +116,33 @@ def sources(self, start = None, end = None): author_extractor('achternaam'), transform=lambda values: [format_name(parts) for parts in zip(*values)] ) - ) + ), + es_mapping=keyword_mapping(enable_full_text_search=True), + visualizations=['resultscount', 'termfrequency'], ) author_id = Field( name='author_id', - extractor=author_single_value_extractor('pers_id') + display_name='Author ID', + description='ID(s) of the author(s)', + extractor=author_single_value_extractor('pers_id'), + es_mapping=keyword_mapping(), ) author_year_of_birth = Field( name='author_year_of_birth', - extractor=author_single_value_extractor('jaar_geboren') + display_name='Author year of birth', + description='Year in which the author(s) was(/were) born', + extractor=author_single_value_extractor('jaar_geboren'), + es_mapping=text_mapping(), ) author_year_of_death = Field( name='author_year_of_death', + display_name='Author year of death', + description='Year in which the author(s) died', extractor=author_single_value_extractor('jaar_overlijden'), + es_mapping=text_mapping(), ) # the above fields are also given as proper dates in geb_datum / overl_datum @@ -111,18 +150,26 @@ def sources(self, start = None, end = None): author_place_of_birth = Field( name='author_place_of_birth', + display_name='Author place of birth', + description='Place the author(s) was(/were) born', extractor=author_single_value_extractor('geb_plaats'), + es_mapping=keyword_mapping(), ) author_place_of_death = Field( name='author_place_of_death', - extractor=author_single_value_extractor('overl_plaats') + display_name='Author place of death', + description='Place where the author(s) died', + extractor=author_single_value_extractor('overl_plaats'), + es_mapping=keyword_mapping(), ) # gender is coded as a binary value (∈ ['1', '0']) # converted to a string to be more comparable with other corpora author_gender = Field( name='author_gender', + display_name='Author gender', + description='Gender of the author(s)', extractor=join_extracted( Pass( author_extractor('vrouw',), @@ -131,55 +178,90 @@ def sources(self, start = None, end = None): values ), ) - ) + ), + es_mapping=keyword_mapping(), + search_filter=MultipleChoiceFilter(), + visualizations=['resultscount', 'termfrequency'], ) url = Field( name='url', - extractor=Metadata('url') + display_name='URL', + description='Link to the book\'s page in DBNL', + extractor=Metadata('url'), + es_mapping=keyword_mapping(), ) url_txt = Field( name = 'url_txt', - extractor=Metadata('text_url') + display_name='URL (txt file)', + description='Link to a .txt file with the book\'s contents', + extractor=Metadata('text_url'), + es_mapping=keyword_mapping(), ) genre = Field( name='genre', - extractor=join_extracted(Metadata('genre')) + display_name='Genre', + description='Genre of the book', + extractor=join_extracted(Metadata('genre')), + es_mapping=keyword_mapping(), + search_filter=MultipleChoiceFilter(), + visualizations=['resultscount', 'termfrequency'], + ) language = Field( name='language', + display_name='Language', + description='Language in which the book is written', extractor=XML( 'language', toplevel=True, recursive=True, - ) + ), + es_mapping=keyword_mapping(), + search_filter=MultipleChoiceFilter(), + visualizations=['resultscount', 'termfrequency'], ) language_code = Field( name='language_code', + display_name='Language code', + description='ISO code of the book\'s language', extractor=XML( 'language', attribute='id', toplevel=True, recursive=True, - ) + ), + es_mapping=keyword_mapping(), ) content = Field( name='content', + display_name='Content', + description='Content of this section', + display_type='text_content', + results_overview=True, + search_field_core=True, + csv_core=True, extractor=XML( tag='p', multiple=True, flatten=True, - ) + ), + es_mapping=main_content_mapping(token_counts=True), + visualizations=['wordcloud', 'ngram'], ) order_in_book = Field( name='order_in_book', + display_name='Order within book', + description='Order of this section within the book', extractor=Index(), + es_mapping=int_mapping(), + sortable=True, ) fields = [ diff --git a/documentation/Defining-corpus-fields.md b/documentation/Defining-corpus-fields.md index 063f3e0f7..4e1b91cef 100644 --- a/documentation/Defining-corpus-fields.md +++ b/documentation/Defining-corpus-fields.md @@ -47,9 +47,9 @@ The following properties determine how a field appears in the interface. `search_filter` can be set if the interface should include a search filter widget for the field. I-analyzer includes date filters, multiplechoice filters (used for keyword data), range filters, and boolean filters. See [filters.py](../backend/addcorpus/filters.py). -`visualizations` optionally specifies a list of visualisations that apply for the field. Generally speaking, this is based on the type of data. For date fields and categorical/ordinal fields (usually keyword type), you can use `['resultcount', 'termfrequency']`. For text fields, you can use `['wordcloud', 'ngram']`. +`visualizations` optionally specifies a list of visualisations that apply for the field. Generally speaking, this is based on the type of data. For date fields and categorical/ordinal fields (usually keyword type), you can use `['resultscount', 'termfrequency']`. For text fields, you can use `['wordcloud', 'ngram']`. -If a field includes the `'resultcount' and/or `'termfrequency'` visualisations and it is not a date field, you can also specify `visualisation_sort`, which determines how to sort the x-axis of the graph. Default is `'value'`, where categories are sorted based on the y-axis value (i.e., frequency). You may specify that they should be sorted on `'key'`, so that categories are sorted alphabetically (for keywords) or small-to-large (for numbers). +If a field includes the `'resultscount'` and/or `'termfrequency'` visualisations and it is not a date field, you can also specify `visualisation_sort`, which determines how to sort the x-axis of the graph. Default is `'value'`, where categories are sorted based on the y-axis value (i.e., frequency). You may specify that they should be sorted on `'key'`, so that categories are sorted alphabetically (for keywords) or small-to-large (for numbers). `search_field_core` determines if a field is listed by default when selecting specific fields to search in. If it is not set to `True`, the user would have to click on "show all fields" to see it. From 89e4bee6568d522d7e683dae49a55ccba966b810 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 4 May 2023 17:28:53 +0200 Subject: [PATCH 037/262] improve source file generation fix filename pattern add progress bar --- backend/corpora/dbnl/dbnl.py | 6 ++++-- backend/requirements.in | 1 + backend/requirements.txt | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index f0d901b82..ea684b034 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -1,5 +1,7 @@ from datetime import datetime import os +import re +from tqdm import tqdm from django.conf import settings from addcorpus.corpus import XMLCorpus, Field @@ -27,9 +29,9 @@ def sources(self, start = None, end = None): csv_path = os.path.join(self.data_directory, 'titels_pd.csv') all_metadata = extract_metadata(csv_path) - for filename in os.listdir(xml_dir): + for filename in tqdm(os.listdir(xml_dir)): if filename.endswith('.xml'): - id, *_ = filename.split('_') + id, *_ = re.split(r'_(?=\d+\.xml$)', filename, maxsplit=1) path = os.path.join(xml_dir, filename) entry_level = find_entry_level(path) metadata = { diff --git a/backend/requirements.in b/backend/requirements.in index 0356e66d8..316ad5ecf 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -23,3 +23,4 @@ celery Redis pypdf2 openpyxl +tqdm diff --git a/backend/requirements.txt b/backend/requirements.txt index c9dd17d14..f9da647d9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -219,7 +219,9 @@ tomli==2.0.1 tornado==6.2 # via django-livereload-server tqdm==4.64.1 - # via nltk + # via + # -r requirements.in + # nltk typing-extensions==4.4.0 # via pypdf2 urllib3==1.26.13 From 703306a4c99c1b5f1c808c27c5e5acc35f8018fc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 5 May 2023 13:02:03 +0200 Subject: [PATCH 038/262] fix content extraction include all types of content tags --- backend/corpora/dbnl/dbnl.py | 3 +- .../dbnl/tests/test_dbnl_extraction.py | 48 +++++++++++++++++++ backend/corpora/dbnl/utils.py | 2 + 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index ea684b034..704033e77 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -249,7 +249,8 @@ def sources(self, start = None, end = None): search_field_core=True, csv_core=True, extractor=XML( - tag='p', + tag=re.compile('^(p|l|head|row)$'), + recursive=True, multiple=True, flatten=True, ), diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 4b90c0de6..9386326af 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -59,9 +59,56 @@ def dbnl_corpus(settings): '\'t Amsterdam Voor Michiel de Groot, Boek-Verkooper op den Nieuwen Dijck, 1671.', ]), 'order_in_book': 0, + }, + { + 'title_id': 'maer005sing01', + 'title': 'Het singende nachtegaeltje', + 'volumes': None, + 'edition': '1ste druk', + 'author_id': 'maer005', + 'author': 'Cornelis Maertsz.', + 'author_year_of_birth': None, + 'author_place_of_birth': 'Wervershoof', + 'author_year_of_death': 'na 1671', + 'author_place_of_death': None, + 'author_gender': 'man', + 'url': 'https://dbnl.org/tekst/maer005sing01_01', + 'url_txt': 'https://dbnl.org/nieuws/text.php?id=maer005sing01', + 'year': '1671', + 'year_full': '1671', + 'genre': 'poëzie', + 'language': 'Nederlands', + 'language_code': 'nl', + 'content': '\n'.join([ + 'Op De vermakelijke en stightelijke Liedekens van Cornelis Maarts', + 'SOo wort de schrand\'re Rey der vloeiende Poëten', + 'Door u, o waerde Vriend! vervult,', + 'Soo wort uw\' Naam met Eer vergult,', + 'En door de Lof-bazuin roem rughtigh uitgekreten,', + 'De Dight-kunst scheen wel eer in Amstel silte Plassen', + 'Alleen te sitten op haer throon,', + 'Maar ghy stelt in uw\' Dight ten toon', + 'Dat in ons Wervershoof nogh eed\'ler vrughten wassen.', + 'Want\'t baat niet dat men kan een yd\'le Pen beswang\'ren', + 'Met wonderlijck Gedight,', + 'Indienmen niet en stight,', + 'Maer met een Heydensch rot vervult de Mond der Sang\'ren.', + 'Ghy soeckt de Af-breuk van het Rijck des Helschen lagers.', + 'Dies ghy een Heiligh Ooghwit raakt,', + 'En onse Ieught sticht en vermaakt', + 'Soo volght ghy \'t saligh Spoor des Ioodschen Harpe-Slagers.', + 'Treet voort dien Eerbaan in, en laat geen Aardsch gewemel', + 'V hind\'ren in soo eed\'len Saak,', + 'Soo streckt uw \'Lands-lien tot een Baak', + 'En voert ons dart\'le Ieughd al singende ten Hemel.', + 'H. Vander Meer.', + ]), + 'order_in_book': 1, } ] + + def test_dbnl_extraction(dbnl_corpus): corpus = load_corpus(dbnl_corpus) docs = list(corpus.documents()) @@ -70,3 +117,4 @@ def test_dbnl_extraction(dbnl_corpus): for actual, expected in zip(docs, expected_docs): assert actual == expected + diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index c23d58f02..90d48fdd9 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -1,6 +1,8 @@ import csv from functools import reduce from bs4 import BeautifulSoup +import re + from addcorpus.extract import Metadata, Extractor, Combined, Pass From a855707ebc9f6ea3b2dc45b16ead9dbe3d30c28f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 5 May 2023 15:07:39 +0200 Subject: [PATCH 039/262] add document context --- backend/corpora/dbnl/dbnl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 704033e77..24d2b9d86 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -24,6 +24,12 @@ class DBNL(XMLCorpus): def tag_entry(self, metadata): return metadata['xml_entry_level'] + document_context = { + 'context_fields': ['title_id'], + 'sort_field': 'order_in_book', + 'context_display_name': 'book' + } + def sources(self, start = None, end = None): xml_dir = os.path.join(self.data_directory, 'xml_pd') csv_path = os.path.join(self.data_directory, 'titels_pd.csv') From 9fdbe879ecbdce8ccf216db7ec50d0bb7965dcd4 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 5 May 2023 15:10:09 +0200 Subject: [PATCH 040/262] include document ID field --- backend/corpora/dbnl/dbnl.py | 10 ++++++++++ backend/corpora/dbnl/tests/test_dbnl_extraction.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 24d2b9d86..248d15e6d 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -71,6 +71,15 @@ def sources(self, start = None, end = None): es_mapping=keyword_mapping() ) + id = Field( + name='id', + extractor=Combined( + Metadata('id'), + Index(transform=str), + transform='_'.join, + ) + ) + volumes = Field( name='volumes', display_name='Volumes', @@ -276,6 +285,7 @@ def sources(self, start = None, end = None): fields = [ title_field, title_id, + id, volumes, edition, year_full, diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 9386326af..d6c10975a 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -35,6 +35,7 @@ def dbnl_corpus(settings): { 'title_id': 'maer005sing01', 'title': 'Het singende nachtegaeltje', + 'id': 'maer005sing01_0', 'volumes': None, 'edition': '1ste druk', 'author_id': 'maer005', @@ -63,6 +64,7 @@ def dbnl_corpus(settings): { 'title_id': 'maer005sing01', 'title': 'Het singende nachtegaeltje', + 'id': 'maer005sing01_1', 'volumes': None, 'edition': '1ste druk', 'author_id': 'maer005', From 369c3952b3528e7db9f11db69748e0af034096c6 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 5 May 2023 15:35:17 +0200 Subject: [PATCH 041/262] support mixed-language books --- backend/corpora/dbnl/dbnl.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 248d15e6d..177650a2f 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -236,6 +236,8 @@ def sources(self, start = None, end = None): 'language', toplevel=True, recursive=True, + multiple=True, + transform=join_values, ), es_mapping=keyword_mapping(), search_filter=MultipleChoiceFilter(), @@ -245,12 +247,17 @@ def sources(self, start = None, end = None): language_code = Field( name='language_code', display_name='Language code', - description='ISO code of the book\'s language', - extractor=XML( - 'language', - attribute='id', - toplevel=True, - recursive=True, + description='ISO code of the text\'s language', + extractor=Backup( + XML( + attribute='lang', + ), + XML( + 'language', + attribute='id', + toplevel=True, + recursive=True, + ), ), es_mapping=keyword_mapping(), ) From a832c189439597048a373c94cc0f29114c982b1a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 5 May 2023 15:46:39 +0200 Subject: [PATCH 042/262] add chapter titles --- backend/corpora/dbnl/dbnl.py | 46 ++++++++++++++----- .../dbnl/tests/test_dbnl_extraction.py | 6 ++- backend/corpora/dbnl/utils.py | 2 + 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 177650a2f..1233dbe39 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -26,7 +26,7 @@ def tag_entry(self, metadata): document_context = { 'context_fields': ['title_id'], - 'sort_field': 'order_in_book', + 'sort_field': 'chapter_index', 'context_display_name': 'book' } @@ -262,6 +262,36 @@ def sources(self, start = None, end = None): es_mapping=keyword_mapping(), ) + chapter_title = Field( + name='chapter_title', + display_name='Chapter', + extractor=Backup( + XML( + tag='head', + recursive=True, + flatten=True, + ), + XML( + tag=LINE_TAG, + recursive=True, + flatten=True, + ) + ), + results_overview=True, + search_field_core=True, + csv_core=True, + visualizations=['wordcloud'], + ) + + chapter_index = Field( + name='chapter_index', + display_name='Chapter index', + description='Order of this chapter within the book', + extractor=Index(transform=lambda x : x + 1), + es_mapping=int_mapping(), + sortable=True, + ) + content = Field( name='content', display_name='Content', @@ -271,7 +301,7 @@ def sources(self, start = None, end = None): search_field_core=True, csv_core=True, extractor=XML( - tag=re.compile('^(p|l|head|row)$'), + tag=LINE_TAG, recursive=True, multiple=True, flatten=True, @@ -280,15 +310,6 @@ def sources(self, start = None, end = None): visualizations=['wordcloud', 'ngram'], ) - order_in_book = Field( - name='order_in_book', - display_name='Order within book', - description='Order of this section within the book', - extractor=Index(), - es_mapping=int_mapping(), - sortable=True, - ) - fields = [ title_field, title_id, @@ -309,6 +330,7 @@ def sources(self, start = None, end = None): genre, language, language_code, + chapter_title, + chapter_index, content, - order_in_book, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index d6c10975a..198fd67c2 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -59,7 +59,8 @@ def dbnl_corpus(settings): 'Cornelis Maertsz. tot Wervers hoof.', '\'t Amsterdam Voor Michiel de Groot, Boek-Verkooper op den Nieuwen Dijck, 1671.', ]), - 'order_in_book': 0, + 'chapter_title': None, + 'chapter_index': 1, }, { 'title_id': 'maer005sing01', @@ -105,7 +106,8 @@ def dbnl_corpus(settings): 'En voert ons dart\'le Ieughd al singende ten Hemel.', 'H. Vander Meer.', ]), - 'order_in_book': 1, + 'chapter_title': 'Op De vermakelijke en stightelijke Liedekens van Cornelis Maarts', + 'chapter_index': 2, } ] diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 90d48fdd9..46749e8c4 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -176,3 +176,5 @@ def find_entry_level(xml_path): format_name = lambda parts: ' '.join(filter(None, parts)) + +LINE_TAG = re.compile('^(p|l|head|row|item)$') From 8b23fbbc1da347500bd33e2fcb5b0887af33983a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 5 May 2023 16:00:14 +0200 Subject: [PATCH 043/262] handle gender of unknown/anonymous authors --- backend/corpora/dbnl/dbnl.py | 9 +++++---- backend/corpora/dbnl/tests/test_dbnl_extraction.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 1233dbe39..70bcf3365 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -182,16 +182,17 @@ def sources(self, start = None, end = None): ) # gender is coded as a binary value (∈ ['1', '0']) - # converted to a string to be more comparable with other corpora + # converted to a string for clarity + # 0 is used for men, unknown/anonymous authors, and institutions author_gender = Field( name='author_gender', display_name='Author gender', description='Gender of the author(s)', extractor=join_extracted( - Pass( - author_extractor('vrouw',), + Pass( # use look-up dict to transform values to string + author_extractor('vrouw'), transform=lambda values: map( - lambda value: {'0': 'man', '1': 'vrouw'}.get(value, None), + lambda gender: {'0': 'man/unknown', '1': 'woman'}.get(gender, None), values ), ) diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 198fd67c2..0a8d9d432 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -44,7 +44,7 @@ def dbnl_corpus(settings): 'author_place_of_birth': 'Wervershoof', 'author_year_of_death': 'na 1671', 'author_place_of_death': None, - 'author_gender': 'man', + 'author_gender': 'man/unknown', 'url': 'https://dbnl.org/tekst/maer005sing01_01', 'url_txt': 'https://dbnl.org/nieuws/text.php?id=maer005sing01', 'year': '1671', From 9c7dd50ccc3a2945a5980f4451a4235cd06d9fb2 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 8 May 2023 11:01:16 +0200 Subject: [PATCH 044/262] add whitespace padding where needed --- backend/addcorpus/extract.py | 5 ++- backend/corpora/dbnl/dbnl.py | 1 + .../dbnl/tests/test_dbnl_extraction.py | 31 ++++++++++++++++++- backend/corpora/dbnl/utils.py | 13 ++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/backend/addcorpus/extract.py b/backend/addcorpus/extract.py index 9e83fa479..efba228a1 100644 --- a/backend/addcorpus/extract.py +++ b/backend/addcorpus/extract.py @@ -252,7 +252,10 @@ def _apply(self, soup_top, soup_entry, *nargs, **kwargs): else: soup = self._select(soup_top if self.toplevel else soup_entry) if self.transform_soup_func: - soup = self.transform_soup_func(soup) + if type(soup) == bs4.element.ResultSet: + soup = [self.transform_soup_func(bowl) for bowl in soup] + else: + soup = self.transform_soup_func(soup) if not soup: return None diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 70bcf3365..9ac740ae1 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -306,6 +306,7 @@ def sources(self, start = None, end = None): recursive=True, multiple=True, flatten=True, + transform_soup_func=compose(tag_padder('cell', ' '), tag_padder('lb', '\n')) ), es_mapping=main_content_mapping(token_counts=True), visualizations=['wordcloud', 'ngram'], diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 0a8d9d432..16a320c35 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -1,8 +1,10 @@ import pytest import os +from bs4 import BeautifulSoup from addcorpus.load_corpus import load_corpus -from corpora.dbnl.utils import extract_metadata, compose +from addcorpus.extract import XML +from corpora.dbnl.utils import extract_metadata, compose, append_to_tag here = os.path.abspath(os.path.dirname(__file__)) @@ -22,6 +24,33 @@ def test_metadata_extraction(): assert multiple_authors['titel'] == 'Spiegel historiael (5 delen)' assert len(multiple_authors['auteurs']) == 3 +append_testcases = [ + ( + 'Vraeghje wie het meeste goedt.107', + 'cell', + ' ', + 'Vraeghje wie het meeste goedt.107', + 'Vraeghje wie het meeste goedt. 107', + ), + ( + 'Nu lokken schone Prenten\nHun beider vrolijke ogen', + 'lb', + '\n', + 'Nu lokken schone Prenten Hun beider vrolijke ogen', + 'Nu lokken schone Prenten\nHun beider vrolijke ogen', + ), +] + +@pytest.mark.parametrize(['xml', 'tag', 'padding', 'original_output', 'new_output'], append_testcases) +def test_append_to_tag(xml, tag, padding, original_output, new_output): + soup = BeautifulSoup(xml, 'lxml-xml') + extractor = XML(flatten=True) + assert extractor._flatten(soup) == original_output + + edited_soup = append_to_tag(soup, tag, padding) + + assert extractor._flatten(edited_soup) == new_output + @pytest.fixture def dbnl_corpus(settings): diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 46749e8c4..eb82a048c 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -2,6 +2,7 @@ from functools import reduce from bs4 import BeautifulSoup import re +import copy from addcorpus.extract import Metadata, Extractor, Combined, Pass @@ -178,3 +179,15 @@ def find_entry_level(xml_path): format_name = lambda parts: ' '.join(filter(None, parts)) LINE_TAG = re.compile('^(p|l|head|row|item)$') + +def append_to_tag(soup, tag, filler): + ''' + Insert a string before each instance of a tag. + ''' + + for tag in soup.find_all(tag): + tag.append(filler) + + return soup + +tag_padder = lambda tag, filler: lambda soup: append_to_tag(soup, tag, filler) From 81c1204a2273fa146722864915bcb85c793cb3fe Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 8 May 2023 11:09:36 +0200 Subject: [PATCH 045/262] make ids more-or-less match dbnl interface --- backend/corpora/dbnl/dbnl.py | 9 +++++---- backend/corpora/dbnl/tests/test_dbnl_extraction.py | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 9ac740ae1..ed07a47fe 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -36,14 +36,15 @@ def sources(self, start = None, end = None): all_metadata = extract_metadata(csv_path) for filename in tqdm(os.listdir(xml_dir)): - if filename.endswith('.xml'): - id, *_ = re.split(r'_(?=\d+\.xml$)', filename, maxsplit=1) + if filename.endswith('.xml'): + id, _ = os.path.splitext(filename) + metadata_id, *_ = re.split(r'_(?=\d+$)', id, maxsplit=1) path = os.path.join(xml_dir, filename) entry_level = find_entry_level(path) metadata = { 'id': id, 'xml_entry_level': entry_level, - **all_metadata[id] + **all_metadata[metadata_id] } year = int(metadata['_jaar']) @@ -75,7 +76,7 @@ def sources(self, start = None, end = None): name='id', extractor=Combined( Metadata('id'), - Index(transform=str), + Index(transform=lambda i: str(i).zfill(4)), transform='_'.join, ) ) diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 16a320c35..14281f2d4 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -62,9 +62,9 @@ def dbnl_corpus(settings): expected_docs = [ { - 'title_id': 'maer005sing01', + 'title_id': 'maer005sing01_01', 'title': 'Het singende nachtegaeltje', - 'id': 'maer005sing01_0', + 'id': 'maer005sing01_01_0000', 'volumes': None, 'edition': '1ste druk', 'author_id': 'maer005', @@ -92,9 +92,9 @@ def dbnl_corpus(settings): 'chapter_index': 1, }, { - 'title_id': 'maer005sing01', + 'title_id': 'maer005sing01_01', 'title': 'Het singende nachtegaeltje', - 'id': 'maer005sing01_1', + 'id': 'maer005sing01_01_0001', 'volumes': None, 'edition': '1ste druk', 'author_id': 'maer005', From d9dcd2f7cef58aef7af3cea0a7eeaee6f0810f3b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 8 May 2023 10:21:36 +0200 Subject: [PATCH 046/262] small improvements & polishing --- backend/corpora/dbnl/dbnl.py | 35 +++---- backend/corpora/dbnl/images/dbnl-logo.jpeg | Bin 9158 -> 0 bytes backend/corpora/dbnl/images/dbnl.png | Bin 0 -> 21155 bytes .../dbnl/tests/test_dbnl_extraction.py | 3 - backend/corpora/dbnl/utils.py | 95 ++++++++++-------- 5 files changed, 66 insertions(+), 67 deletions(-) delete mode 100644 backend/corpora/dbnl/images/dbnl-logo.jpeg create mode 100644 backend/corpora/dbnl/images/dbnl.png diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index ed07a47fe..52aa7ce8d 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -2,27 +2,26 @@ import os import re from tqdm import tqdm +import random from django.conf import settings from addcorpus.corpus import XMLCorpus, Field -from addcorpus.extract import Metadata, XML, Pass, Index +from addcorpus.extract import Metadata, XML, Pass, Index, Backup, Combined from corpora.dbnl.utils import * from addcorpus.es_mappings import * from addcorpus.filters import RangeFilter, MultipleChoiceFilter class DBNL(XMLCorpus): title = 'DBNL' - description = 'Digitale Bibliotheek voor de Nederlandse letteren' + description = 'Digital Library for Dutch Literature' data_directory = settings.DBNL_DATA min_date = datetime(year=1200, month=1, day=1) max_date = datetime(year=2020, month=12, day=31) es_index = getattr(settings, 'DBNL_ES_INDEX', 'dbnl') - image = 'dbnl-logo.jpeg' + image = 'dbnl.png' tag_toplevel = 'TEI.2' - - def tag_entry(self, metadata): - return metadata['xml_entry_level'] + tag_entry = { 'name': 'div', 'attrs': {'type': 'chapter'} } document_context = { 'context_fields': ['title_id'], @@ -38,12 +37,10 @@ def sources(self, start = None, end = None): for filename in tqdm(os.listdir(xml_dir)): if filename.endswith('.xml'): id, _ = os.path.splitext(filename) - metadata_id, *_ = re.split(r'_(?=\d+$)', id, maxsplit=1) + metadata_id, *_ = re.split(r'_(?=\d+$)', id) path = os.path.join(xml_dir, filename) - entry_level = find_entry_level(path) metadata = { 'id': id, - 'xml_entry_level': entry_level, **all_metadata[metadata_id] } @@ -104,12 +101,14 @@ def sources(self, start = None, end = None): year_int = Field( name='year', display_name='Publication year (est.)', - description='Year of publication as a number. May not be exact.', + description='Year of publication as a number. May not be an estimate.', extractor=Metadata('_jaar'), es_mapping=int_mapping(), search_filter=RangeFilter(lower=1200, upper=2020), visualizations=['resultscount', 'termfrequency'], sortable=True, + visualization_sort='key', + primary_sort=True, ) edition = Field( @@ -211,14 +210,6 @@ def sources(self, start = None, end = None): es_mapping=keyword_mapping(), ) - url_txt = Field( - name = 'url_txt', - display_name='URL (txt file)', - description='Link to a .txt file with the book\'s contents', - extractor=Metadata('text_url'), - es_mapping=keyword_mapping(), - ) - genre = Field( name='genre', display_name='Genre', @@ -227,7 +218,6 @@ def sources(self, start = None, end = None): es_mapping=keyword_mapping(), search_filter=MultipleChoiceFilter(), visualizations=['resultscount', 'termfrequency'], - ) language = Field( @@ -251,10 +241,10 @@ def sources(self, start = None, end = None): display_name='Language code', description='ISO code of the text\'s language', extractor=Backup( - XML( + XML( # get the language on chapter-level if available attribute='lang', ), - XML( + XML( #otherwise, get the (first) language for the book 'language', attribute='id', toplevel=True, @@ -297,7 +287,7 @@ def sources(self, start = None, end = None): content = Field( name='content', display_name='Content', - description='Content of this section', + description='Text in this chapter', display_type='text_content', results_overview=True, search_field_core=True, @@ -329,7 +319,6 @@ def sources(self, start = None, end = None): author_place_of_death, author_gender, url, - url_txt, genre, language, language_code, diff --git a/backend/corpora/dbnl/images/dbnl-logo.jpeg b/backend/corpora/dbnl/images/dbnl-logo.jpeg deleted file mode 100644 index 36198d43ba46b192ba00109ca84a409d8299176a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9158 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 diff --git a/backend/corpora/dbnl/images/dbnl.png b/backend/corpora/dbnl/images/dbnl.png new file mode 100644 index 0000000000000000000000000000000000000000..e957311f13d8e62dc0d4033309a2fd1938db5911 GIT binary patch literal 21155 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 paragraphs +- headers +- line (used for poems/songs) +- table rows (used for poems/songs) +- list items +''' -def append_to_tag(soup, tag, filler): +def append_to_tag(soup, tag, padding): ''' - Insert a string before each instance of a tag. + Insert a string at the end of each instance of a tag. ''' for tag in soup.find_all(tag): - tag.append(filler) + tag.append(padding) return soup -tag_padder = lambda tag, filler: lambda soup: append_to_tag(soup, tag, filler) +tag_padder = lambda tag, padding: lambda soup: append_to_tag(soup, tag, padding) +''' +Unary shorthand: apply append_to_tag with a particular tag and padding string. +''' From 842813726507ca6327b1cdbe0a7ebde703f300df Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 8 May 2023 15:09:15 +0200 Subject: [PATCH 047/262] also include metadata-only records --- backend/addcorpus/es_mappings.py | 3 + backend/corpora/dbnl/dbnl.py | 76 ++++++++++++++++--- .../dbnl/tests/test_dbnl_extraction.py | 47 +++++++----- backend/corpora/dbnl/utils.py | 21 +++++ 4 files changed, 114 insertions(+), 33 deletions(-) diff --git a/backend/addcorpus/es_mappings.py b/backend/addcorpus/es_mappings.py index d8f126434..b2a350edb 100644 --- a/backend/addcorpus/es_mappings.py +++ b/backend/addcorpus/es_mappings.py @@ -72,3 +72,6 @@ def int_mapping(): return { 'type': 'integer' } + +def bool_mapping(): + return {'type': 'boolean'} diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 52aa7ce8d..1129bfff5 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -9,7 +9,7 @@ from addcorpus.extract import Metadata, XML, Pass, Index, Backup, Combined from corpora.dbnl.utils import * from addcorpus.es_mappings import * -from addcorpus.filters import RangeFilter, MultipleChoiceFilter +from addcorpus.filters import RangeFilter, MultipleChoiceFilter, BooleanFilter class DBNL(XMLCorpus): title = 'DBNL' @@ -30,24 +30,47 @@ class DBNL(XMLCorpus): } def sources(self, start = None, end = None): - xml_dir = os.path.join(self.data_directory, 'xml_pd') csv_path = os.path.join(self.data_directory, 'titels_pd.csv') all_metadata = extract_metadata(csv_path) - for filename in tqdm(os.listdir(xml_dir)): - if filename.endswith('.xml'): - id, _ = os.path.splitext(filename) - metadata_id, *_ = re.split(r'_(?=\d+$)', id) - path = os.path.join(xml_dir, filename) + print('Extracting XML files...') + for id, path in tqdm(list(self._xml_files())): + metadata_id, *_ = re.split(r'_(?=\d+$)', id) + csv_metadata = all_metadata.pop(metadata_id) + metadata = { + 'id': id, + 'has_xml': True, + **csv_metadata + } + + year = int(metadata['_jaar']) + + if between_years(year, start, end): + yield path, metadata + + # we popped metadata while going through the XMLs + # now add data for the remaining records (without text) + + print('Extracting metadata-only records...') + with BlankXML(self.data_directory) as blank_file: + for id in tqdm(all_metadata): + csv_metadata = all_metadata[id] metadata = { 'id': id, - **all_metadata[metadata_id] + 'has_xml': False, + **csv_metadata } - year = int(metadata['_jaar']) - if between_years(year, start, end): - yield path, metadata + yield blank_file, metadata + + def _xml_files(self): + xml_dir = os.path.join(self.data_directory, 'xml_pd') + for filename in os.listdir(xml_dir): + if filename.endswith('.xml'): + id, _ = os.path.splitext(filename) + path = os.path.join(xml_dir, filename) + yield id, path title_field = Field( name='title', @@ -279,7 +302,10 @@ def sources(self, start = None, end = None): name='chapter_index', display_name='Chapter index', description='Order of this chapter within the book', - extractor=Index(transform=lambda x : x + 1), + extractor=Index( + transform=lambda x : x + 1, + applicable=lambda metadata: metadata['has_xml'] + ), es_mapping=int_mapping(), sortable=True, ) @@ -303,6 +329,30 @@ def sources(self, start = None, end = None): visualizations=['wordcloud', 'ngram'], ) + has_content = Field( + name='has_content', + display_name='Content available', + description='Whether the contents of this book are available on I-analyzer', + extractor=Metadata('has_xml'), + es_mapping=bool_mapping(), + search_filter=BooleanFilter( + true='Content available', + false='Metadata only' + ), + ) + + is_primary = Field( + name='is_primary', + display_name='Primary', + description='Whether this is the primary document for this book - each book has only one primary document', + extractor=Index(transform = lambda index : index == 0), + search_filter=BooleanFilter( + true='Primary', + false='Other', + description='Select only primary documents - i.e. only one result per book', + ) + ) + fields = [ title_field, title_id, @@ -325,4 +375,6 @@ def sources(self, start = None, end = None): chapter_title, chapter_index, content, + has_content, + is_primary, ] diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 41d9b084e..a9f287b8b 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -88,26 +88,10 @@ def dbnl_corpus(settings): ]), 'chapter_title': None, 'chapter_index': 1, + 'has_content': True, + 'is_primary': True, }, { - 'title_id': 'maer005sing01_01', - 'title': 'Het singende nachtegaeltje', - 'id': 'maer005sing01_01_0001', - 'volumes': None, - 'edition': '1ste druk', - 'author_id': 'maer005', - 'author': 'Cornelis Maertsz.', - 'author_year_of_birth': None, - 'author_place_of_birth': 'Wervershoof', - 'author_year_of_death': 'na 1671', - 'author_place_of_death': None, - 'author_gender': 'man', - 'url': 'https://dbnl.org/tekst/maer005sing01_01', - 'year': '1671', - 'year_full': '1671', - 'genre': 'poëzie', - 'language': 'Nederlands', - 'language_code': 'nl', 'content': '\n'.join([ 'Op De vermakelijke en stightelijke Liedekens van Cornelis Maarts', 'SOo wort de schrand\'re Rey der vloeiende Poëten', @@ -134,17 +118,38 @@ def dbnl_corpus(settings): ]), 'chapter_title': 'Op De vermakelijke en stightelijke Liedekens van Cornelis Maarts', 'chapter_index': 2, + 'is_primary': False, + } +] + [{}] * 68 + [ # skip to the next book + { + 'title_id': 'maer002alex01', + 'title': 'Alexanders geesten', + 'year_full': '13de eeuw', + 'year': '1200', + 'author_id': 'maer002', + 'author': 'Jacob van Maerlant', + 'url': 'https://dbnl.org/tekst/maer002alex01_01', + 'content': None, + 'has_content': False, + 'is_primary': True, + }, { # book with multiple authors + 'title_id': 'maer002spie00', + 'author_id': 'maer002, uten001, velt003', + 'author': 'Jacob van Maerlant, Philip Utenbroecke, Lodewijk van Velthem', + 'author_year_of_birth': 'ca. 1230, ?(13de eeuw), ca. 1270', + 'author_place_of_birth': None, } ] - def test_dbnl_extraction(dbnl_corpus): corpus = load_corpus(dbnl_corpus) docs = list(corpus.documents()) - assert len(docs) == 70 + assert len(docs) == 70 + 6 # 70 chapters + 6 metadata-only books for actual, expected in zip(docs, expected_docs): - assert actual == expected + # assert that actual is a superset of expected + assert expected.items() <= actual.items() + diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 93ea29b40..ede8b91e7 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -1,6 +1,9 @@ import csv from functools import reduce import re +from bs4 import BeautifulSoup +import os + from addcorpus.extract import Metadata, Pass @@ -121,6 +124,24 @@ def update_data_with_row(data, row): return data +# === METADATA-ONLY RECORDS === + +class BlankXML: + def __init__(self, data_directory): + self.filename = os.path.join(data_directory, '_.xml') + + def __enter__(self): + # create an xml that will generate one "spoonful", i.e. one document + # but no actual content + soup = BeautifulSoup('
', 'lxml-xml') + with open(self.filename, 'w') as file: + file.write(soup.prettify()) + + return self.filename + + def __exit__(self, exc_type, exc_value, traceback): + os.remove(self.filename) + # === UTILITY FUNCTIONS === def compose(*functions): From 53fa28834335f5deb2e881820fc9af17092ecb39 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 8 May 2023 15:36:55 +0200 Subject: [PATCH 048/262] add description page --- backend/corpora/dbnl/dbnl.py | 3 ++- backend/corpora/dbnl/description/dbnl.md | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 backend/corpora/dbnl/description/dbnl.md diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 1129bfff5..6fae3d419 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -16,9 +16,10 @@ class DBNL(XMLCorpus): description = 'Digital Library for Dutch Literature' data_directory = settings.DBNL_DATA min_date = datetime(year=1200, month=1, day=1) - max_date = datetime(year=2020, month=12, day=31) + max_date = datetime(year=1890, month=12, day=31) es_index = getattr(settings, 'DBNL_ES_INDEX', 'dbnl') image = 'dbnl.png' + description_page = 'dbnl.md' tag_toplevel = 'TEI.2' tag_entry = { 'name': 'div', 'attrs': {'type': 'chapter'} } diff --git a/backend/corpora/dbnl/description/dbnl.md b/backend/corpora/dbnl/description/dbnl.md new file mode 100644 index 000000000..e67b98059 --- /dev/null +++ b/backend/corpora/dbnl/description/dbnl.md @@ -0,0 +1,15 @@ +### About DBNL + +The Digital Library of Dutch Literature ([DBNL](https://www.dbnl.org/)) is a digital collection of texts from Dutch literature, linguistics, and cultural history, from the earliest period to the present. The collection represents the whole of the Dutch language area. DBNL is a collaboration between the [Taalunie](https://taalunie.org/), the [Vlaamse Erfgoedbibliotheken](https://vlaamse-erfgoedbibliotheken.be/), and the [KB, the Dutch Royal Library](https://www.kb.nl/). + +### What can you find in the DBNL dataset? + +The DBNL dataset can be used for research into Dutch and Flemish linguistics and literature, from the middle ages to the present. Limburghish, Frisian, Surinam, and South African literature are represented. + +The dataset contains digitised texts, which have been manually corrected, with metadata. It include medieval literature as well as classic novels. In addition, the dataset contains magazines from Dutch language and literary studies, such as De Gids and De Revisor. + +### Availability + +The I-analyzer corpus contains the publicly available portion of the [DBNL-dataset](https://www.kb.nl/onderzoeken-vinden/datasets/dbnl-dataset). The texts are in the public domain. + +For some books, the public dataset provides metadata but not the full text. In documents with metadata only, the full text is usually available on the DBNL interface. From 6a6fcbcc669dcfc750a09f0e578d8686e81c285d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 11:17:22 +0200 Subject: [PATCH 049/262] improve language extraction, add language metadata --- backend/corpora/dbnl/dbnl.py | 37 +++++++++++++--- .../dbnl/tests/test_dbnl_extraction.py | 2 +- backend/corpora/dbnl/utils.py | 42 +++++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 6fae3d419..ebd318dd0 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -21,6 +21,9 @@ class DBNL(XMLCorpus): image = 'dbnl.png' description_page = 'dbnl.md' + languages = ['nl', 'dum', 'fr', 'la', 'fy', 'lat', 'en', 'nds', 'de', 'af'] + category = 'book' + tag_toplevel = 'TEI.2' tag_entry = { 'name': 'div', 'attrs': {'type': 'chapter'} } @@ -248,12 +251,28 @@ def _xml_files(self): name='language', display_name='Language', description='Language in which the book is written', - extractor=XML( - 'language', - toplevel=True, - recursive=True, - multiple=True, - transform=join_values, + # this extractor is similar to language_code below, + # but designed to accept multiple values in case of uncertainty + extractor=join_extracted( + Backup( + XML( # get the language on chapter-level if available + attribute='lang', + transform=lambda value: [value] if value else None, + ), + XML( # look for section-level codes + {'name': 'div', 'attrs': {'type': 'section'}}, + attribute='lang', + multiple=True, + ), + XML( # look in the top-level metadata + 'language', + toplevel=True, + recursive=True, + multiple=True, + attribute='id' + ), + transform = lambda codes: map(language_name, codes) if codes else None, + ) ), es_mapping=keyword_mapping(), search_filter=MultipleChoiceFilter(), @@ -264,16 +283,22 @@ def _xml_files(self): name='language_code', display_name='Language code', description='ISO code of the text\'s language', + # as this may be used to set the HTML lang attribute, it forces a single value extractor=Backup( XML( # get the language on chapter-level if available attribute='lang', ), + XML( # look for section-level code + {'name': 'div', 'attrs': {'type': 'section'}}, + attribute='lang' + ), XML( #otherwise, get the (first) language for the book 'language', attribute='id', toplevel=True, recursive=True, ), + transform=compose(standardize_language_code, single_language_code), ), es_mapping=keyword_mapping(), ) diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index a9f287b8b..a4c27807b 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -77,7 +77,7 @@ def dbnl_corpus(settings): 'year': '1671', 'year_full': '1671', 'genre': 'poëzie', - 'language': 'Nederlands', + 'language': 'Dutch', 'language_code': 'nl', 'content': '\n'.join([ 'Het singende Nachtegaeltje', diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index ede8b91e7..e13751128 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -225,3 +225,45 @@ def append_to_tag(soup, tag, padding): ''' Unary shorthand: apply append_to_tag with a particular tag and padding string. ''' + +def standardize_language_code(code): + # ISO 639-1 -> 639-3 + replacements = { + 'fy': 'fry', + 'la': 'lat', + } + + if code in replacements: + return replacements[code] + + return code + +def single_language_code(code): + if code and '-' in code: + primary, *rest = code.split('-') + return primary + return code + +LANGUAGE_NAMES = { + 'nl': 'Dutch', + 'fr': 'French', + 'lat': 'Latin', + 'fry': 'Frisian', + 'en': 'English', + 'nds': 'Low German', + 'de': 'German', + 'af': 'Afrikaans', + 'rus': 'Russian', + None: None, +} + +def language_name(code): + if not code: + return None + codes = code.split('-') + standardized = map(standardize_language_code, codes) + names = set(map( + lambda code: LANGUAGE_NAMES.get(code, code), + standardized + )) + return '/'.join(names) From 6a2a05b82385171abe6334a8420ea3e0f4628c42 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 11:53:14 +0200 Subject: [PATCH 050/262] improve extraction of periodicals --- backend/corpora/dbnl/dbnl.py | 11 +++++++ backend/corpora/dbnl/tests/data/titels_pd.csv | 3 +- .../dbnl/tests/test_dbnl_extraction.py | 8 +++++ backend/corpora/dbnl/utils.py | 33 ++++++++++++++++--- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index ebd318dd0..5179755aa 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -146,6 +146,16 @@ def _xml_files(self): es_mapping=text_mapping(), ) + periodical = Field( + name='periodical', + display_name='Periodical', + description='Periodical in which the text appeared', + extractor=Metadata('periodical'), + es_mapping=keyword_mapping(), + search_filter=MultipleChoiceFilter(), + visualizations=['resultscount', 'termfrequency'], + ) + author = Field( name='author', display_name='Author', @@ -385,6 +395,7 @@ def _xml_files(self): id, volumes, edition, + periodical, year_full, year_int, author, diff --git a/backend/corpora/dbnl/tests/data/titels_pd.csv b/backend/corpora/dbnl/tests/data/titels_pd.csv index 23f58d586..df776e670 100644 --- a/backend/corpora/dbnl/tests/data/titels_pd.csv +++ b/backend/corpora/dbnl/tests/data/titels_pd.csv @@ -8,4 +8,5 @@ ti_id|titel|vols|jaar|druk|ppn_o|bibliotheek|categorie|_jaar|pers_id|voornaam|vo "maer002spie05"|"Spiegel historiael. Derde partie"||"ca. 1283-1296"|"handschrift"|||"1"|"1283"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002spie05_01"|||"poëzie"| "maer002spie06"|"Spiegel historiael. Vierde partie"||"ca. 1283-1296"|"handschrift"|||"1"|"1283"|"maer002"|"Jacob"|"van"|"Maerlant"|"ca. 1230"|"ca. 1300"||||"Damme"|||"damme001"||"0"|"https://dbnl.org/tekst/maer002spie06_01"|||"poëzie"| "maer005sing01"|"Het singende nachtegaeltje"||"1671"|"1ste druk"|"393478793"|"denha004koni01"|"1"|"1671"|"maer005"|"Cornelis"||"Maertsz."|"?"|"na 1671"|||"Wervershoof"||"werve001"||||"0"|"https://dbnl.org/tekst/maer005sing01_01"|"https://dbnl.org/nieuws/text.php?id=maer005sing01"|"2012_10 "|"poëzie"| -"maer005stic01"|"Stichtelijcke gesangen"||"1661"|"1ste druk"|"393478807"|"leide001univ01"|"1"|"1661"|"maer005"|"Cornelis"||"Maertsz."|"?"|"na 1671"|||"Wervershoof"||"werve001"||||"0"|"https://dbnl.org/tekst/maer005stic01_01"|"https://dbnl.org/nieuws/text.php?id=maer005stic01"|"2013_02 "|"poëzie"| +"will028belg00"|"Belgisch museum voor de Nederduitsche tael- en letterkunde en de geschiedenis des vaderlands"||"1837-1846"|"1ste druk"|"394987047"||"1"|"1837"|"will028"|"J.F."||"Willems"|"1793"|"1846"|"11 maart"|"24 juni"|"Boechout"|"Gent"|"boech001"||"gent_001"||"0"|"https://dbnl.org/tekst/will028belg00_01"|||"proza"| +"will028belg00"|"Belgisch museum voor de Nederduitsche tael- en letterkunde en de geschiedenis des vaderlands"||"1837-1846"|"1ste druk"|"394987047"||"1"|"1837"|"_bel001"|||"[tijdschrift] Belgisch Museum"|||||||||||"0"|"https://dbnl.org/tekst/will028belg00_01"|||"proza"| diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index a4c27807b..815585649 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -66,6 +66,7 @@ def dbnl_corpus(settings): 'id': 'maer005sing01_01_0000', 'volumes': None, 'edition': '1ste druk', + 'periodical': None, 'author_id': 'maer005', 'author': 'Cornelis Maertsz.', 'author_year_of_birth': None, @@ -139,6 +140,13 @@ def dbnl_corpus(settings): 'author_year_of_birth': 'ca. 1230, ?(13de eeuw), ca. 1270', 'author_place_of_birth': None, } +] + [{}] * 3 + [ + { # periodical + 'title_id': 'will028belg00', + 'author_id': 'will028', + 'author': 'J.F. Willems', + 'periodical': 'Belgisch Museum', + } ] diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index e13751128..6549de577 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -78,6 +78,18 @@ def add_metadata_row(data, row): return data +def row_periodical(row): + ''' + If the row's author field describes a periodical, return its name (None otherwise) + ''' + + prefix = '[tijdschrift]' + name = row['achternaam'] + + if name.startswith(prefix): + return name[len(prefix):].strip() + + def row_author_data(row): '''Dict with all the author metadata in a row''' return { @@ -89,7 +101,13 @@ def row_author_data(row): def data_from_row(row): '''Construct a new metadata item from a csv row.''' - author = row_author_data(row) + periodical = row_periodical(row) + + if not periodical: + authors = [row_author_data(row)] + else: + authors = [] + plurals = { key: [value] for key, value in formatted_items(row) @@ -102,7 +120,8 @@ def data_from_row(row): } return { - 'auteurs': [author], + 'auteurs': authors, + 'periodical': periodical, **plurals, **rest } @@ -118,9 +137,13 @@ def update_data_with_row(data, row): if row[key] not in data[key]: data[key].append(empty_to_none(row[key])) - author = row_author_data(row) - if author not in data['auteurs']: - data['auteurs'].append(author) + periodical = row_periodical(row) + if periodical: + data['periodical'] = periodical + else: + author = row_author_data(row) + if author not in data['auteurs']: + data['auteurs'].append(author) return data From 58d3db04a5a16de7cdd3414b3d9a8337667726ac Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 12:12:34 +0200 Subject: [PATCH 051/262] add search filter descriptions --- backend/corpora/dbnl/dbnl.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 5179755aa..1a76ba31b 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -131,7 +131,10 @@ def _xml_files(self): description='Year of publication as a number. May not be an estimate.', extractor=Metadata('_jaar'), es_mapping=int_mapping(), - search_filter=RangeFilter(lower=1200, upper=2020), + search_filter=RangeFilter( + description='Select books by publication year', + lower=1200, upper=2020 + ), visualizations=['resultscount', 'termfrequency'], sortable=True, visualization_sort='key', @@ -152,7 +155,9 @@ def _xml_files(self): description='Periodical in which the text appeared', extractor=Metadata('periodical'), es_mapping=keyword_mapping(), - search_filter=MultipleChoiceFilter(), + search_filter=MultipleChoiceFilter( + description='Select texts from periodicals', + ), visualizations=['resultscount', 'termfrequency'], ) @@ -235,7 +240,9 @@ def _xml_files(self): ) ), es_mapping=keyword_mapping(), - search_filter=MultipleChoiceFilter(), + search_filter=MultipleChoiceFilter( + description='Select books based on the gender of the author(s)', + ), visualizations=['resultscount', 'termfrequency'], ) @@ -253,7 +260,9 @@ def _xml_files(self): description='Genre of the book', extractor=join_extracted(Metadata('genre')), es_mapping=keyword_mapping(), - search_filter=MultipleChoiceFilter(), + search_filter=MultipleChoiceFilter( + description='Select books in these genres', + ), visualizations=['resultscount', 'termfrequency'], ) @@ -285,7 +294,10 @@ def _xml_files(self): ) ), es_mapping=keyword_mapping(), - search_filter=MultipleChoiceFilter(), + search_filter=MultipleChoiceFilter( + description='Select books in these languages', + option_count=20, + ), visualizations=['resultscount', 'termfrequency'], ) @@ -372,6 +384,7 @@ def _xml_files(self): extractor=Metadata('has_xml'), es_mapping=bool_mapping(), search_filter=BooleanFilter( + description='Select books with text available on I-analyzer, or metadata-only books', true='Content available', false='Metadata only' ), From 11e743756d0fd1f882c302dcd5a2f638a9f62e0a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 13:21:29 +0200 Subject: [PATCH 052/262] simplify code --- backend/corpora/dbnl/dbnl.py | 33 ++++++++++--------- .../dbnl/tests/test_dbnl_extraction.py | 5 +-- backend/corpora/dbnl/utils.py | 18 +++------- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 1a76ba31b..a1400148e 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -306,21 +306,24 @@ def _xml_files(self): display_name='Language code', description='ISO code of the text\'s language', # as this may be used to set the HTML lang attribute, it forces a single value - extractor=Backup( - XML( # get the language on chapter-level if available - attribute='lang', - ), - XML( # look for section-level code - {'name': 'div', 'attrs': {'type': 'section'}}, - attribute='lang' - ), - XML( #otherwise, get the (first) language for the book - 'language', - attribute='id', - toplevel=True, - recursive=True, + extractor=Pass( + Backup( + XML( # get the language on chapter-level if available + attribute='lang', + ), + XML( # look for section-level code + {'name': 'div', 'attrs': {'type': 'section'}}, + attribute='lang' + ), + XML( #otherwise, get the (first) language for the book + 'language', + attribute='id', + toplevel=True, + recursive=True, + ), + transform=single_language_code, ), - transform=compose(standardize_language_code, single_language_code), + transform=standardize_language_code, ), es_mapping=keyword_mapping(), ) @@ -371,7 +374,7 @@ def _xml_files(self): recursive=True, multiple=True, flatten=True, - transform_soup_func=compose(tag_padder('cell', ' '), tag_padder('lb', '\n')) + transform_soup_func=pad_content, ), es_mapping=main_content_mapping(token_counts=True), visualizations=['wordcloud', 'ngram'], diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 815585649..33c7a9e5e 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -4,13 +4,10 @@ from addcorpus.load_corpus import load_corpus from addcorpus.extract import XML -from corpora.dbnl.utils import extract_metadata, compose, append_to_tag +from corpora.dbnl.utils import extract_metadata, append_to_tag here = os.path.abspath(os.path.dirname(__file__)) -def test_compose(): - assert compose(str.upper, ' '.join)(['a', 'b']) == 'A B' - def test_metadata_extraction(): csv_path = os.path.join(here, 'data', 'titels_pd.csv') data = extract_metadata(csv_path) diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 6549de577..df33755b4 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -167,14 +167,6 @@ def __exit__(self, exc_type, exc_value, traceback): # === UTILITY FUNCTIONS === -def compose(*functions): - ''' - Given a list of unary functions, returns a new function that is the composition of all - - e.g. compose(str.upper, ' '.join)(['a', 'b']) == 'A B' - ''' - return lambda y: reduce(lambda x, func: func(x), reversed(functions), y) - def author_extractor(field): ''' Create an extractor for one field in the author metadata @@ -208,7 +200,7 @@ def join_extracted(extractor): ''' return Pass(extractor, transform=join_values) -author_single_value_extractor = compose(join_extracted, author_extractor) +author_single_value_extractor = lambda key: join_extracted(author_extractor(key)) def between_years(year, start_date, end_date): if start_date and year < start_date.year: @@ -244,10 +236,10 @@ def append_to_tag(soup, tag, padding): return soup -tag_padder = lambda tag, padding: lambda soup: append_to_tag(soup, tag, padding) -''' -Unary shorthand: apply append_to_tag with a particular tag and padding string. -''' +def pad_content(node): + pad_cells = lambda n: append_to_tag(n, 'cell', ' ') + pad_linebreaks = lambda n: append_to_tag(n, 'lb', '\n') + return pad_cells(pad_linebreaks(node)) def standardize_language_code(code): # ISO 639-1 -> 639-3 From 6465686021aa2dc7315097430ef10600df87826c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 14:14:55 +0200 Subject: [PATCH 053/262] fix index extractor for csv corpora --- backend/addcorpus/corpus.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index 31f75720b..7c64c0dfd 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -619,7 +619,7 @@ def source2dicts(self, source): # make sure the field size is as big as the system permits csv.field_size_limit(sys.maxsize) for field in self.fields: - if not isinstance(field.extractor, ( + if isinstance(field.extractor, ( extract.HTML, extract.XML )): raise RuntimeError( @@ -638,7 +638,8 @@ def source2dicts(self, source): reader = csv.DictReader(f, delimiter=self.delimiter) document_id = None rows = [] - for i, row in enumerate(reader): + index = 0 + for row in reader: is_new_document = True if self.required_field and not row.get(self.required_field): # skip row if required_field is empty @@ -653,12 +654,13 @@ def source2dicts(self, source): document_id = identifier if is_new_document and rows: - yield self.document_from_rows(rows, metadata, i) + yield self.document_from_rows(rows, metadata, index) rows = [row] + index += 1 else: rows.append(row) - yield self.document_from_rows(rows, metadata) + yield self.document_from_rows(rows, metadata, index) def document_from_rows(self, rows, metadata, row_index): doc = { From fc712777a4ae5f145adc3b970e2dd747642720b2 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 9 May 2023 16:37:43 +0200 Subject: [PATCH 054/262] use shortert test file --- .../tests/data/xml_pd/maer005sing01_01.xml | 4179 ----------------- .../dbnl/tests/test_dbnl_extraction.py | 19 +- 2 files changed, 15 insertions(+), 4183 deletions(-) diff --git a/backend/corpora/dbnl/tests/data/xml_pd/maer005sing01_01.xml b/backend/corpora/dbnl/tests/data/xml_pd/maer005sing01_01.xml index fe91c7960..d9ada5c61 100644 --- a/backend/corpora/dbnl/tests/data/xml_pd/maer005sing01_01.xml +++ b/backend/corpora/dbnl/tests/data/xml_pd/maer005sing01_01.xml @@ -205,4046 +205,6 @@ H. Vander Meer.
- - -Toe-eygeninge -
-Aen de singende Ieught. -SOete jonckheyt, groene spruyties, -Die uyt keelties, soet als fluyties, -Met een suyck'ren gallem singht, -Dat het al in vreught op springht: -Ionge Dochters, soete liefies, -Minnaers-veulties, herte diefies, -Die door Honich soet geluyt, -Treckt mijn hert ter ooren uyt, -En laet het in wellust semmen, -Ende lobb'ren in u stemmen, - -Ende baden in de klanck, -Van uw' ziel-suygende sanck -t' Wijl dat op u roode lipjes -Even als koralen klipjes, -'t Swacke hobbelende boot -Van mijn hert, in stucken stoot. -Aengename Iongelingen, -Afgeveerdight tot het singen, -Minnaers van dat soet geslacht, -Dat ick mee seer hooge acht. -Ghy wordt nu van my gebeden, -Om wat by my in te treden: -Koom dan binnen, want ick nu -Eensjes singen sal voor u. -
-
- - -[O Soete Ieught! Die in u jeughde zijt] -
-Stemme: Periosta. -O Soete Ieught! Die in u jeghde zijt -Des werelts pronck: en yder 't herte steelt, -Als ghy met sangh, in uwe vreugde blijdt, -Door soeten galm, veel droeve smerten heelt, -Ghy jaegt my oock mijn leet ter kelen uyt, -Door het geschal, op Keel en Veel, en Fluyt. -2 Niet als ghy singht 't geen tegen deughden strijt, -En door ontucht de kuyssche ziele smet: -Ach! doet dat niet, schoon ghy in vreugden zijt, -Satan dees strick tot u vernielen set, -En 't is vergift voor yder eerbaer hert, -'t Welck knaegt de deugt tot sy onweerbaer wert - -3 Maer als ghy sticht al wie u singen hoort, -En deught, en vreugt, al met malkander mengt -En Godes lof seer soet doet bringen voor, -Of nutte leer, den een aen d' ander schenckt: -En offert soo u stem, en sijnen keel, -In uwe jeught, aen Godt tot sijnen deel. -4 Voor u o jeught, die na dees dingen poogt, -Dicht ick onlanghs seer menigh stichtlijck liet, -'t Welck is een werk daer uyt ghy singhen mooght -Wat goets en soets: in't welck ghy lichtlijck siet -Wat dat de reyne lust, en vreughde scheelt, -Van 't geen door-gaens de yd'le ieughde speelt. -5 Wel aen, ghy schoone, en ghy nette ieught, - -Ick schenck dit werck aen u uyt goeden wil, -'t Welck u niet toont een vuyl besmette vreught, -Maer't hout de lusten in haer woeden stil, -'t Wijl 't door vermaeck doet vry van smerten gaen: -Wel neem dan dit geschenck van herten aen, -  -Van u lieven, ende waertsten, -Die u mint, -  -Cornelis Maertsen. - -
-
-
- - -
-De stemme der Wijsheyt. Proverb. 1. 20. -Stemme: O Karsnacht: -DE wijsheydt die is uytgevaren, -Om haer aen 't volck te openbaren, -En daerom roeptse over-luyt -In al de poorten van de steden, -En al waer vele menschen treden, -Aldus voor 't volck haer reden uyt: - -2 Hoe lang, o dwase, domme sinnen! -Wilt ghy de dwaesheydt dus beminnen, -En gaen op spotters wegen voort: -Sal noyt de tucht uw' sin op-scherpen? -Of sult ghy alle tijdt verwerpen, -Des Heeren wil, en wet, en woort? -3 Wild och uw' gangen omme-keeren, -En schikt u gangh nae mijn begeeren, -En neemt de tucht van herten aen: -En ick sal alsdan u verklaren -Des levens padt, en openbaren -Aen u, den weg die ghy moet gaen. -4 Maer als ick roep uyt alle krachten, -En niemandt op mijn stem wilt achten, -Noch luysteren na mijn geseght: -En streck mijn handt uyt haer te vaten, - -En niemandt hem wil leyden laten, -Noch wijcken van sijn boosen weg: -5 Soo sal ick dan, als haer comt drucken -Den noodt, in doodt, en ongelucken, -Oock lacchen in haer tegenspoen: -En helpen niet wanneerse klagen: -Maer spotten dan, wanneer de plagen, -Van schrick, en angst haer sterven doen. -6 Daerom dat sy de ware leere, -Aen haer gegeven van den Heere, -Niet hebben in haer doen betracht: -En daer-en-boven, t' allen tijden, -Al mijn straffen en kastijden, -Niet aengenomen, maer veracht. -7 Laet haer dan wederom ter degen, -De vruchten van haer boose wegen - -Op eten, totse worden sat: -Want onheyl, ramp, en ongelucken, -Die spruyten uyt al sulcke stucken, -En wassen, op 't Godloose pat. -
-
-
- -Volmaekten zegen. Psal. 144:12. 15. -
-Stem: Ghy loderlijcke Sylvia. -O Groot geluck! Voor die 't geniet, -O! zegen rijcke stroomen, -Die in sijn huys, sijn Soonen siet -Op wassen, als de Boomen: -Dat is een saeck // daer door vermaeck, -En eere werde becoomen. -2. Geluckigh, die behalven dien, -Sijn dochters magh aenschouwen, - -(Daer in men 't beelde des deughts can sien) -Als steenen uyt-gehouwen, -Die kant, en net // den Meester set, -In kostlijcke gebouwen. -3. En die sijn Schuuren zijn vol graen, -Daer elck vindt sijn behagen: -Diens Schapen op de Weyden gaen, -En duysent iongen dragen: -Daer door 't getal // vermeeren sal, -En wassen alle dagen. -4. Die sijn Ossen even staegh -Gaen in de velden ploegen, -Dat sy haer Heere alle daegh -Veel vruchten door toevoegen: -Die soo bevint // hoe veel hy wint, -En schept daer in genoegen. - -5. die van geen schade, noch verlies, -Sijn leven comt te hooren, -Noch krijght geen tijdingh onder dies -Dat hy wat heeft verlooren: -Noch geenen tijdt // een moort-gekrijt -Komt klincken in sijn ooren. -6. Geluckigh is een Mensch sijn staet, -Die dit is toe-gevallen: -Maer Saligh, in den hooghsten graet, -Ia Saligh boven allen -Is hy, die Godt // self tot een lot, -En deel is toe-gevallen. -
-
-
- -May Liet -
-Stemme: Edel Karsouw. - -DE soete Mey -Verciert het velt met bloemen, -En alderhande kruyt, -Seer veelderley -Dat niet en is te noemen, -Nu uyt daer aerde spruyt. -De lucht is vol g'luyt, -Vol sangh, door 't quinckeleeren -Van 't gediert // dat hene swiert -Op hare rasse veeren. -2. De tijdt voor heen, -De Lente, en de Winter, -Ons nu niet meerder bijt: -Maer yder een -Nu tegenwoordigh vindter -Al gants een ander tijdt - -Weest nu oock verblijt, -En wilt den Heere loven, -Want al't goet, dat ons ontmoet -Dat komt alleen van boven. -3. Al wat verdort -Sijn cieraet had verlooren -En 't leven was ontgaen, -Dat selve wordt -Nu wederom herbooren, -En komt uyt 't graf opstaen, -Kruyt, en Gras en Graen, -Steeckt nu het hooftjen buyten, -En in jeught, vol soete vreught, -Komt lieffelijck uyt-spruyten. -4. Lof zy u Heer, -Bestuurder der nature, - -Die aen ons staegh gedenckt: -En altijdt weer -Na 's Winters kout besuren -De soete Mey ons schenckt, -En den Somer brenght, -Die met sijn rijcke gaven, -Ons staegh doet // met overvloet -En alle noodruft laven. -5. Kroont weer dit Iaer -Met u goedtgunsticheden, -En sent u zegen neer, -Geeft oock daer naer, -Dat wy in rust en vreden -Besorgen uwe eer: -Ende geeft, o Heer; -Dat wy na u behagen. - -In 't gemoet, staegh vruchten goedt, -Tot uwer eere dragen. -
-
-
- -Minne-spiegel. -
-Stem: Ick quam in een Boomgaerdetjen. -MEn siet de Min, en haren dwangh, -In Iacob in haer kracht, -Een dienst van veerthien jaren langh, -Wort by hem kort geacht, -Alleenelijck om dat hy tot sijn loone -Den minnelijcken Rachel wacht. -2. Als niemandt hem onthouden dorst -Ontrent het open velt, -Wt oorsaeck dat de koude vorst -Daer bits, en vinnich knelt, - -Brant hy van liefde, ende wordet nimmer -Ia sijn verhert gedult ontstelt. -3. Als oock de Son door heeten brant -De nacht uyt 't velt verjaeght, -En door sijn vlam doet spleten 't landt, -Hy 't altemael verdraght: -Noch ys, noch vyer en kond hem nauliks krenken -Door suyv're liefde tot die maeght. -4. Wie is hy, die niet loven sal -Dees Minnaer, en Vriendin: -Geluckigh zijn so boven al, -Die dus uyt reyne min -Te samen voegen, herten ende handen: -Ick wensch geen ander Huys-gesin. -
-
-
- -Dina geschent -
- -Het Singende - -Stemme: O boole domme jeught. -GHy Dochters jongh, en teer, -Die geern vermeyen gaet, -Komt siet eens hoe u eer, -Dan in perijckel staet, -En hoe dit soet vermaeck -Vermenghelt is met gal -Dees' saeck, dees' saeck, dees' saeck, -Ick u vertoonen sal. -3. Dina niet wel bedacht, -Vingh eens het reysen aen, -En lette op de dracht, -Daer mee de Meysjes gaen: -En om oock self daer by, -Wat meerder nieuws te doen, -Kleed sy, kleed sy, kleed sy - -Haer in een vreemt fatsoen. -3. Doe sy dus moy, en schoon, -De Landen ommegingh, -Soo saegh haer Hemors Soon, -Een dertel iongelingh, -Die met sijn ooge sweeft, -Ontrent haer moye dracht, -En heeft, en heeft, en heeft -Die schoone Maeght verkracht -4. Doe dit nu was geschiet, -Denck eens, wat ongemack, -Dat onheyl, en verdriet, -Vyt dese daet ontstack: -Daer wort veel bloot gestort, -En wie in Sichem woont, -Die wort, die wort, die wort, - -Niet van het sweert verschoont. -5. Twee Broeders seer verstoort, -Om dese vuyle schant, -Begingen dese moort, -Self met haer eygen handt. -Den ouden Iacob treurd, -En nam't geweldigh swaer, -En scheurd, en scheurd, en scheurd, -Het hert schier van malkaer. -6. En Dina mist haer eer, -Daer elck sijn roem op draeght, -En sy en word niet meer -Gereeckent voor een Maeght -Niet een woordt meer en staet, -Van haer, den Bybel deur: -Ick haet, dees daet, en laet -Voortaen oock na van heur. -
-
-
- - - - - -Op een Swellingh aen mijn Aengesicht. -
-Stemme: Ach droom hoe quelt ghy mijn gedachten? -WAt is ieught, en't leflijck bloosen -Van haren verw? 't zin meerder niet als Roosen -Die wel schoon, staen ten toon, -Cierelijck te proncken, -Root als bloodt // wonder soet // ende doet -Het gesicht van yder daer op loncken: -Maer als 't onweer daer opblaest, -Dan is al haer schoonheydt haest, - -Geheel te niet, als ware het versoncken. -2. Even alsoo zijn de schoonheden: -Een moy aenschijn, het ciersel van de leden, -Dat nu staet // tot cieraet, -Als een Son te schijnen, -Kloec en hel // eenen swel // terstont fel, -Al de schoonheydt veerdigh doen verdwijnen: -'t Blomtien dat soo aerdigh tierd, -Is dan in der haest ontcierd, -Men siet het sonder schoonheyt treurigh quijnen. -3. Sulcken les kan ick heden lesen -In mijn aensicht, en in mijn eygen wesen: -Want als ick // my bekick, -'k Sie dan mijn wanghen, -Noch onlangh // glad en blanck, nu vol stanck, -Ende vol van buylen etter hangen: - -Soo dat als men my besiet, -Ick gelijck my selven niet: -Maer 't schijnt, ik heb een ander hooft ontfangen. -4. Druck dit in 't hert, ghy jonge lieden, -En leert hier door, de trots, en hoogh-moet moet vlieden -Draeght geen roem // op een bloem -Teerder als de Roosen, -V aenschijn // kan haest zijn // gelijck mijn, -Als een bloemtjen dat nu is bevroosen -'s Werelts schoon, en't jeughlijck moy, -Dat kan even als het hoy, -Sijn groen verliesen, in seer korte poosen. -Den 25. Iannarius 1671. -
-
-
- -Davids Huwelijck met Abigael. -
- -Stemme: Ick roep u Hemelsche Vader aen. -EEn goet verstant, een wijs beleyt, -Verciert ons 't gantsche leven: -En die sijn dingen met bescheyt, -In goede billickheyt, -Voorsichtigh wel beleyt, -Die wordt daer door verheven -2. En 't is een saeck die seker gaet, -Wt wrevelmoedigh wesen, -Dat daer uyt voor gevolgh ontstaet, -Dat yder een hem haet: -Het welcke is een quaet, -Dat elck behoort te vreesen. -3. Een groote straf, een swaer verdriet, -Heeft David in sijn tooren, - -den Nabal (die hem niet ontsiet, -dat hy een volck verstiet, -daer van hy dinst geniet) -Seer heftelijck gesworen. -4. Maer Abigail gingh hem te moet, -Om hem eerst aen te spreken, -En doe sy viel den Vorst te voet, -Veranderd' hem 't gemoet, -Soo datse david doet, -Sijn quaden aenslagh breken. -5. door haer soet en vriend'lijk gelaet, -En haer geschickte zeden: -En daer en boven door haer daet, -Soo maecktse dat het quaet -Van haer geslachte gaet: -En dit was hare reden. -
-
- - -Stemme: Schoonste Nymphje in het wout. -ACh! gesalfde van den Heer, -Vol van eer, -Hoor u Maeght geduldigh spreken, -Nabal is een heyloos man, -Daerom dan, -Laet doch na om dat te wreken. -2. Nabal deed u dit verdriet, -Daer ick niet -Wiste van de gantsche sake: -Toornt om soo een sot niet meer -Vromen Heer, -God bewaer u voor dees wraek. -3. God is Richter over al, - -En hy sal -Selver Nabal daer om straffen: -Siet, hier brengh ick een geschenck, -Ende denck, -Dit u krijghs-volck te beschaffen. -4. Wreeckt doch lieve David niet -V verdriet, -Nabal is u roed ontwossen: -God die selver voor ugaet, -Sal uyt smaet, -En verdriet u eens verlossen. -5. Dit te wreken is Gods saeck, -Laet de wraeck, -Dan aen hem bevolen blijven: -Ende soo wanneer als dan -Een Tyran - -V soud soecken te verdrijven, -6. Sal u ziele vry van last, -Sterck en vast, -In Gods hoopken zijn gebonden -Maer u boose vyant sal -Over al -Zijn geslingert in het ronde. -7. Ende als de grooten Godt, -V stelt tot -Eenen Heer, en Vorst van allen, -Soo en sal't knagen noyt, -Datje oyt, -Yemant in u toorn deed' vallen. -8. En de Heer op 's Hemels troon, -Sal tot loon, -V sijn milde zegen schencken: - -En daer na soo sult ghy weer, -Vromen Heer, -Vwe Maeght in gunst gedencken. -
-
- -Stem: Roosemont die lagh gedoocken. -DOe de Koningh dit aenhoorde, -Liet hy van de wrake of, -En na 't sluyten van haer woorde -Riep hy uyt tot haren lof: -d'Alderhoogste lof en eer, -Zij bewaert voor God den Heer: -2. Maer naest hem, zijt ghy gezegent, -O! ghy zegen-rijcke mont: -Waerje my hier niet bejegent, -'k Had u volck in 't hert gewont, - -En u mannelijck geslacht, -d' Een, met d' ander omgebracht. -3. Oock ontfingh hy haer geschencken, -Ende seyde: Treckt nu voort, -En wilt om geen onheyl dencken, -Want ick heb u stem verhoort: -Gaet na huys, en weest gerust -Want mijn toorn is uyt-geblust. -4. Doe de Son nu met sijn wagen -Thienmael had rondom gereen, -Heeft God self den dwaes geslagen, -Nabals hert word als een steen, -En het leven tornt hem af, -Ende Nabal zijght in't graf. -5. Dit klonck strack in alle ooren, -Nabals wreetheyt is ten ent, - -Yder een quam dit te vooren, -En 't word David oock bekent: -Doe hy nu dees saeck verstont, -Opend' hy aldus sijn mont: -6. Wie en soud u, Heer, niet prijsen? -Noch u geven lof, en danck? -Wie soud u geen eer bewijsen, -Al sijn leve dagen lanck? -Ghy hebt al mijn spt, en smaet, -Nu gewroocken metter daet. -7. Heere, gy deed my beletten, -Dat ick in mijn ongedult, -My niet in u plaets gingh setten -'t Welck geweest had sware schult: -Wat my soo een mensche doet -Ghy, O Heere! wreecken moet. - -8. Ghy neemt oock de wraeck in hande, -Ende wreeckt mijn swaer verdriet: -Nabal maeckt ghy heel te schande, -En ghy brenght hem gants te niet, -Ia verslaet hem in het stof. -V zy Heere, Eeuwigh Lof. -
-
- -Stem: Laura sat laest aen een Beeck. -DAvid die nu had verstaen -Van des Nabals haestigh sterven, -Dachte strack van stonden aen: -Nu sal ick het best verwerven, -Van al Nabals rijcke erven, -Dat is sijnen wijsen Vrou: -Daer wil ick door mijn Booden - -Abigael laten nooden, -Tot mijn gade in de Trou. -2. Hier op geeft hy sijn bevel, -Aen de dienaers van de Koningh, -En sy reysen ras, en snel, -By de Vrou in hare wooningh, -En met een beleefde tooningh, -Seggen sy dit voor haer uyt: -Weest gegroet, ghy wijse Vrouwe: -David die versoeckt in trouwe, -V te hebben tot sijn Bruydt. -3. Hier op sijne op haer wangh, -Roode pleckjes voort-gebroocken, -Evenwel ten duurd niet langh -Of sy heeft haer mondt ontloocken, -En in vreughden uyt-gesproocken: - -Ick ben tot dees saeck bereyt. -Daer mee reysden sy te samen, -Tot dat sy by David quamen, -Daer hy hare komst verbeydt. -4. Daer wordt dese wijse Vrou, -Met de Vorst gepaert in dughden, -En het gantsche landt wort nou -Wacker, en sprong op van vreughde: -Yder een hem seer verheugde, -Om dat Abigail niet meer, -Sal een stuuren kop verdragen, -Maer een wijsen Vorst behagen, -Siet, dus seltsaem wreckt de Heer. -
-
-
- - -Minnaers Compasjen. -
-Stem: Poliphemus aen de strande. -DOe de hoogen Godt op eerde -Eerst formeerde -'t Edel menschelijck geslacht, -Heeft hy Adam, en sijn Vrouwe, -In de trouwe. -Met sijn eygen handt gebracht. -2. Noch op heden, in de sinne, -Woont de minne -Daer door dat het wijf, en man, -d' Een aen d' ander geeft te smaken. - -Sulck vermaken -Als haer niemandt geven kan. -2. Dese lust, des menschen leven: -In geschreven -Doet hem soecken sonder rust, -Tot hy in 't getal der menschen -Na sijn wenschen, -Vint sijn lief, en herten lust. -4. Maer in't soecken, en bejagen -Moet we vragen -'t Oogemerck van ware deught, -Soo sal onse trouw ons geven -Al ons leven, -Reyne lust, en vaste vreught, -5. 't Is veel grooter schat van weerden -Als op eerden - -Al de rijckdom geeft aen ons, -Datmen in ons jonge ieughde -Reyne deughde -Beyde brenght op 't pluymen-dons. -
-
-
- -Rey der Israelitische Vrouwen. 1. Samuel 18:6. -
-Stem: Als Bocksvoetje speelt &c. -ALs Saul, en David den vyant in 't velt -Verioegen, en sloegen -Haer met gewelt, -En quamen in 't Hof // soo worde haer lof, - -Door 't gantsche landt Cana seer hoogh vermelt. -2. De vrouwen in't Israelitische landt, -Die namen, te samen -Den vedel in d'handt, -En springende voort // men singende hoort -Aldus haer schateren door het landt. -3. Weest wellekom Vorsten van Iacobs geslacht -Ghy helden, in velden, -Seer dabber geacht; -Den vyandt besweeck // wanneer hy u keeck, -Noch meer wanneer hy beproefde u macht. -4. Wel laet ons nu Saul veel lof, en veel eer -Bewijsen, en prijsen -Die dapperen Heer, -Die selver op 't landt // met eygener handt, -In't Heydens geslacht sloegh duysent ter neer. - -5. Maer singt noch veel hooger, o vrouwen gedans -Wy moete, begroete, -En setten de krans -Op David sijn hooft: Een yder nu looft -Dees eenighst, dees Fenix, het puyck der Mans. -6. Hy heefter thien-duysent alleenlijck gedoot -En velde, die helde -Seer machtigh en groot: -V daden zijn meer // als Sauls, uw' Heer, -g' Hebt thiemael, den Philisteen meer ontbloot. -
-
-
- -Salomons Gebedt. 2. Paral.1. -
-Stem: Roosemont. Waer ghy vliet. - -DEs Hemels licht verdween, -Doe het groote licht verscheen, -Dat te boven gaet, de vlammende Son, -En hem openbaerd aen Solomon: -Namentlijck doe God, quam van boven af, -En hem dit antwoordt gaf: -Salomon eyscht ghy -Nu vrymoedelijk van my, -Wat dat u lust, en wat het zy. -2. Salomon hier op seydt: -Ghy hebt die Barmhertigheydt -Aen David gedaen, dat ick sijnen Soon, -Na hem ben gevolght, of sijnen troon, -Tot een Hooft van 't volck: en ik ben noch ionck, -En 't volck dat ghy my schonck, -Is sulck een getal, - -Dat niemant tellen sal, -En desen wachten op my al. -3. Daerom ghy wijsen Heer, -Sendt op my een straeltjen neer -Van u Hemels licht: op dat ick nu voort, -Dit volck leyden magh, soo het behoort. -Doe antwoorde Godt: nadien dat het wit -Van 't gene ghy my bidt, -Niet is machtigh goedt, -Noch oock niet uw's vyandts bloedt, -Noch niet dat ghy langh leven moet. -4. Maer alleen dat verstant -In u herte zy geplant, -Dat ghy Iocobs, stam, leyden meught in dees, -Na mijn wil, en woort, in mijne vrees. -Soo geef ick u verstant, wijsheyt, overvloet, - -Rijckdom, schatten, en goet, -En oock lof, en eer, -Alsoo datmen sulcken Heer -Niet en sal vinden immermeer. -5. Hier mee de Heer verdween, -En voer na den Hemel heen. -Ende Salomon, kreegh oock van den Heer, -Wijsheydt, en verstant, rijckdom, en eer: -Ia sulck een verstant, datmen nergens niet -Sijnes gelijcken siet -Silver, ende Gout, -Ende kostlijck Ebben hout, -Kreegh hy oock mede menigh-fout. -6. Salomon hier in speelt -Aen ons allen een voorbeelt, -Dat hy het verstant, acht in sijn gemoet, - -Meerder weert te zijn, als 't aertsche goedt. -Daer is oock geen schat, ons soo nut als dit: -Want die dien schat, besit -Heeft een instrument, -Daer door dat hy tot hem went, -Edele schatten sonder ent. -
-
-
- -Gierigheydt. -
-Stem: Van de drie Dortsche Maeghden. -EEn Mensch die hoord te weten, -Datmen niet leeft om t' eeten, -maer eet op dat men leeft: -Dit moetmen wel bemercken - -In alle onse wercken, -Wat oorsaeck wercken heeft. -2. Maer die dit niet wel weten, -En deerlijck is beseten -Met dwase gierigheyt, -Die werckt niet om te leven, -Maer heeft hem gants begeven -Te leven om arbeydt. -3. Hy slaet hem self met roeden, -En leeft dus in armoeden, -Soo lange tot hy sterft: -En geeft niemant in't leven, -Maer moet het alles geven, -Aen eenen die het erft. -4. Dus dunckt my kan ick mercken, -Dat hy is als een vercken, - -Dat, t' wijle dat het leeft -Ons niet met al doet geven: -Maer na zijn gnortigh leven -Ons vele leckers geeft. -5. Hy is oock als de Peerde, -Die ploegen gaen op eerde, -Met moeylijck ongenucht: -Maer van 't verdrietigh ploeghen, -Van 't sweeten, en van 't swoegen, -En treckse self geen vrucht. -6. Men siet oock in hem blijcken, -Een Esel sijng practijcken, -Die dickmael seer bedroft, -Met sware ongemacken, -Draeght kostelijcke packen, -Daer hy noyt self van proeft. - -7. My denckt hy doet oock mede -Gelijck een Hondt eens dede, -Die op een Hoy-hoop lagh, -Waer hy niet van woud' eten, -En heeft wel luyd' gekreten, -Wanneer hy yemandt sagh. -8. jammerlijcke menschen: -Die staegh om rijckdom wenschen, -En woelen sonder rust, -Om eens vermaeck te vatten, -Maer al des werelt schatten -Verzaken noyt haer lust. -
-
-
- - -Minnaers Noort-ster. -
-Stem: Het daget in den Osten. -MY dunckt dat eenen veugel, -Ellendigh is geplaecht, -Die niet meer als een vleugel -Op sijn kleyn lijfjen draeght: -Die kan hem na behagen -Niet wegh dragen. -2. Soo is 't oock met een herre -Die op een wiegel loopt, -Want sulck een rijedt niet verre -Of is wel dra gesloopt, -Dan scheurt het een, en't ander -Van malkander. - -3. Hier op soo denck ick stracken, -Hier aen sien ick nou, -Het leet, en ongemacken, -Des menschen buyten trou: -Want hem ontmoeten alle -Ongevalle. -4. De man met sijne Vrouwe, -Die is een vleesch, en been, -En daerom buyten trouwe, -Is elck de helft van een: -Dus moet we dese deelen -t' Samen heelen. -5. En soecken na genoegen -Een ander weder-helft, -Om die ons toe te voegen, -En t' smelten aen ons self, - -Om dus twee halve saken -Een te maken. -6. Alleen staet dit te myen, -Dat sonder goedt bestuur, -Wy niet bestaen te vryen -Een ongelijck partuur: -Dat soud' ons onlust geven -Al ons leven. -7. Men siet het noyt gebeuren -Dat yemandt Laken sneedt, -Van twee verscheyden kleuren, -En nayd' het aen een kleedt: -Of die dit doen, zijn sotten, -Meet te spotten. -8. Of saeghje twee Hant-schoene, -d' een nieu, en d' ander oudt, - -d' Een geel, en d' ander groene, -Ick weet, ghy seggen soudt, -Dit past niet, d' een, en d' ander, -By malkander. -9. Kiest dan tot u vriendinne, -Een die u wel gelijckt, -Opdat u hert, en sinne, -Van haer niet af en wijckt, -Maer dat gy liefd' mooght dragen, -Al u dagen. -
-
-
- -Geestelijck Snoep-mes Matth.3:7.8.9 -
-Stem: Florida, soo het wesen magh. - -DOe eertijdts Iohannes vernam -Dat menigh tot sijn Doopsel quam -Met Phariseus gebreecken, -En dat hy sagh, oock meenigh een -'t Welck was een Saduceen, -Begon hy dus te spreecken: -2. Ghy boose Adderen geslacht, -Wie heeft de bootschap u gebracht, -Om Godes toorn t' ontvluchten? -Indien ghy soeckt na Godts genaet, -Soo toont ons metter daet -Nu boetveerdige vruchten. -3. En seght niet: wy zijn soo een stam, -Diens wortel is den Abraham, -Dit magh geen reden strecken: -Want soo ons Godt sijn macht liet sien, - -Hy kond uyt soo een stien, -Een Abraham kindt verwecken. -4. Ick segh u volgens mijn bescheyt, -De Bijl is heden aen-geleyt, -En veerdigh zijn de handen. -Om uyt te roepen onverschoont -Wat Boom geen vreucht en toont, -Om die dan te verbranden. -5. Ick doope wel voor u gesicht, -Een doop, doe ons tot boet verplicht? -Maer die my na komt loopen, -Die is veel weerdiger als ick, -Die sal sijn volk gelick, -Met sijn geest, en vyer doopen. -6. Sijn wan heeft hy in d' hant geree, -Te suyveren sijn dersch daer mee: - -En sal met sijne handen -De Terw vergaren in de schuur, -Maer sal het kaf in't vuur, -In eeuwigheydt doen branden. -
-
-
- -Dwase Hoovaerdigheydt. -
-Stemme: Prins Robbaert. -EEn Aexter was eens bloot en kael, -En had niet op sijn huyt, -Sijn bonte veeren altemael -Waren ghevallen uyt, -Dies klaeghde hy met groot hert-seer -De vogelen zijn leet - -Woud' schencken tot sijn kleet. -2. Een yder was hier willigh toe, -En hebben dat gedaen, -Op dat hy in sijne arremoe -Van koud' niet sou vergaen: -Dit alle vog'len 't samen doen, -Elck pluckt een veertjen uyt, -Soo dat men root, en geel, en groen, -Sagh blincken op sijn huyt. -3. Doe nu den Aexter hem bekeeck -Hy van hem self verschiet, -En seyd, wie is't, die my geleeck? -Ick vind soo eenen niet: -Dus komt, o Vogels! nu ten toon, -Ghy alle te gelick, -En siet of yemandt wel soo schoon - -En cierlijck pronckt als ick. -4. Daer quaem wel haest een grooten som, -Van vogels kleyn, en groot, -Elck nam sijn pluymtjen wederom, -Daer word' hy weder bloot, -En stont daer met sijn naeckten huyt, -Gants kael en sonder kleet. -Hy schreyde bey sijn oogen uyt, -Maer 't holp niet tot sijn leet. -5. Dit toont so aerdigh als het mach, -Een pronckaert sijn bedrijf, -Die naeckt, en kael, quam voor den dagh, -En had gants niet om 't lijf: -Maer op dat hy dus niet versmacht, -En door de koud' vergaet, -Soo neemt hy meenigh beest sijn vacht - -En maeckt hem een gewaet. -6. Als nu den pronckaert, dit gewaet, -Rondom sijn lijf bekijckt, -Soo denckt hy met een trots gelaet, -Wie is't, die my gelijckt: -Maer dat elck 't sijne na hem trock, -Dat hy hem heeft gedaen, -Daer soud' den geck tot yders jock, -Met bloote billen staen. -
-
-
- -Hooghmoedt voor den Val. -
-Stemme: Prins Robbert. -EEn Aep die eenen vreemden lust -In sijn gedachten kreegh, - -Was in hem zelven niet gerust, -Te blijven hier om leegh, -Hy sagh de swier der Vog'len aen, -Tot boven in de Lucht, -En dachte, ick wil mede gaen, -En trecken op de vlucht, -2. Hy maeckte vleugelen van Was, -Aen elcke zyde een, -En daer mee was hy wel te pas, -Want daer op vloogh hy heen: -De grillen in sijn malle kruyn, -Die dreven hem om hoogh, -Geen spitse toorn, noch hooge duyn, -Daer hy niet op en vloogh. -3. Ten lesten door sijn dwase sucht -Die dagelijcks aenwon, - -Soo dacht hy met een snelle vlucht -Te vliegen op de Son: -Maer desen dwasen overlegh, -Die word' in't eynde vals, -De Son die smolt zijn vleug'len wegh, -Hy viel, en brack sijn hals. -4. Een die met ander lieden gelt -Hem meent te maken rijck, -Door dien dat hem den hooghmoet quelt, -Is oock dees Aep gelijck: -Hy met een op-geblasen moet, -t' Onvreden in sijn staet, -Gaet hoopen t' samen goedt op goedt, -En staegh hy hooger gaet, -5. Dan ist dat hy met nijdt aensiet, -De gene die regeert, - -En hoe een yder 't hoogh gebiedt -Met kromme knien eert, -Dies Fyselt hy hem selven op, -Tot hooge macht en eer, -Tot dat hy op den hooghsten top, -Sit proncken als een Heer. -6. Maer wanneer hy in hooge macht -De Son wil zijn gelijck, -En over al de swarte nacht -Wil jaghen uyt sijn rijck, -Alleen door glans van sijn cieraet, -Soo comt des Hemels Heer, -En smelt sijn trotsen, hoogen staet: -Daer Valt den Aep dan neer. -
-
-
- - -Boere Philosophy. -
-Stemme: Princesse hier koom ick by nacht. -WIlje wercken op goe voet, -Dan soo moet -Ghy wel letten wat elck doet: -Ghy moet yder Beest bestuure -Na de drift, na di drift van sijn natuure. -2. Heb je eenen moedigh Paert, -Schatten waert, -Laet het volgen sijnen aert, -En laet het u selven dragen, -Of het moet of het moet gaen voor de wagen. -3. Hebj' een Os, soo stelt hem vroegh -Voor de ploegh, - -Want het is hem spuls genoegh: -Maer kan hy dat niet betrachten, -Weyt hem ver, weyt hem ver, en wilt hem slachten -4. Hebje Koeyen, geeft haer Gras, -Op haer pas, -Anders het noyt voordeel was: -Want sy moeten der van leven, -En dan oock, en dan oock haer Melleck geven -5. Hebje Verckens, geeft haer veel -Garsten meel, -Yder rijckelijck sijn deel: -Geeft haer wey van uwe Koeyen, -Want sy moet, want sy moeter vet van groeyen. -6. Maer indien ghy hebt een Kat, -Geeft het wat -Van het witte Koeye nat: - -Laet het woonen in u huysen, -Want het vanght, want het vangter vele Muysen. -7. En indien ghy hebt een Hoen, -Wilt het voen: -Wat sal 't arme diertjen doen? -Daer en valt niet op te seggen, -Want het doet, want het doet, ons Eyers leggen. -8. Ende hebje noch wat meer, -Als een Heer, -Houdt u Beestjes frays in eer: -Want men moet na reden leven, -Self een Beest, self een Beest het sijne geven. -
-
-
- - -Wereldts ghewoel. -
-Stem: Te may als al de vogelen singen. -HEt is een wonder om aen te mercken, -Hoe vele, en hoe verscheyden wercken -De menschen wel aenvaten: -Een yder die voeght hem tot het sijn, -In hoog, en lage staten, -2. Wanneer als wy de landen doorloopen, -Wat siene wy al menschen met hoopen, -In vaerten, en op wegen, -Die sommige loopen met ons heen, -And're komen ons tegen. -3. Dat ick eens op de maen mochte klimmen, -Wanneer als hy op rijst in de kimmen, - -En gaen met hem om hoogen, -Hoe frap soud' ick dit wereldts gewoel -Aenschouwen met mijn oogen? -4. Maer holla neen, ick heb my versonnen, -Wanneer ick dus was rondom geronnen -Soud' ick niet ander seggen -Als, 'k heb een krimmelende Mieren-hoop -Vol van mieren sien leggen. -
-
-
- -Bekeeringe. -
-Stem: Wel op, wel op, ick ga ter jacht: -KEerom, keerom, o wereldts Kint: -Die de sonden seer bemint: - -Ghy gaet dwalen -Buyten palen -Buyten 't spoor, en buyten 't padt, -Dat ingaet in des Hemels, Stadt: -2. 't Geselschap dat u hier toe-lacht, -Dat wordt in de Hel verwacht, -En 't vermaken, -Dat sy smaken, -Sal haer op-breecken seer suur, -Hier namaels in het Helsche vuur. -3. Wijckt dan af van den boosen hoop -En kiest ghy een ander loop, -Eer sy hooren -Godes cooren, -Die opvaren doen vergramt, -Als eenen vuur seer brandigh vlamt. - -4. Valt dan oock met een yder aen, -Om u sonden te verslaen, -Heel te mortel, -Dat den wortel -Wt u hert magh zijn geroyt, -En die vyanden heel verstroyt. -5. Die door dick, en door dun soo heen -Heeft een vuylen wegh gereen, -En tot boven -Is bestoven -Laet dien wegh alleen niet naer -Maer maeckt hem oock van smeten klaer. -6. Soo zijn door sonden meenigh smet, -Leelijck aen de ziel geset: -Wilje wesen -Reyn van desen, - -Veeght dan al de smetten af, -Die oyt de sond' u ziele gaf. -7. Docht t' wijlwe door dit sterck fenijn, -Selver sonder krachten zijn, -Moet je klagen, -Ende vragen, -Waer is sulcken wijsen geest, -Die my van dit gebreck geneest? -8. Dan sulje hooren eenen stem, -Tot u roepen, (dat van hem, -Die de smerten Vwer herten -Kan omkeeren in genucht) -Komt tot my al die droevigh sucht -
-
-
- - -De [y]delhe[y]t der Rijckdommen. -
-Stem: Doe ic lest wandeld[e] over de Helder -DAer zijn geen saucen die soeter doen smaken -Als doet den honger, die 't alles maeckt soet: -Daer is geen suycker, die 't drinken doet maken -Soo soet, en liflijck, als dorste wel doet. -Oock kan om schatten, den machtigsten Vorst, -Niet koopen honger, noch soeten dorst -Want dese beyde zijn meerder in weert, -Als al de schatten hier op der eerdt. -2. Yemandt verleckert op soete lusten, -Schud vry sijn bedde van pluymen wel sacht, -Nochtans sal hy niet vrediger rusten, -Als eenen Boer, slapend' op 't stroo by nacht. - -Men kan om schatten, noch groote rijckdom, -Geen slaep becomen in eygendom, -Noch rusie koopen: ja 't is altemet, -Dat rijckdom selver den slaep belet. -3. En ook een mantel die rammelt van Goude -En blinckt gelijcken de mane nu vol, -Beschut niet meerder voor bijtende koude, -Als slechte kleeren, van Schapen haer wol. -En oock de huysen aen d' Hemelen hoogh, -Decken niet beter, noch schuylen droogh, -Als eenen stulpjen, of Boere Hoy-schuur, -Het hout is mermer, als steenen muur. -4. Ook smaeckt het drincken uyt gouden vaten -Niet soeter, als uyt eenen pot van aerdt: -Sy smake de min in hoogere staten -Niet soeter, dan offer een Huys-man paerd: - -En groote kassen, vol silver gestoud, -Zijn niet van nooden tot onderhoud. -Een mensch sal leven van 't gene hem voedt, -Maer noyt sijn dage van overvloedt. -5. Daerom soo zijn het verdwalende menschen -Die altijdt woelen om rijckdom met smert. -Ick wil betrachten, en meerder niet wenschen, -Als noodtdroft, ende genoegen in 't hert. -Want ick en brochte ter wereldt gans niet, -Niemant in't sterven behouter yet. -Derhalven niemandt die meerder en heeft, -Als dat alleenlijck, daer hy van leeft. -
-
-
- -Klachte der Godloosen. -
-Stemme: Lest lagh ick onder eenen boom, - -WAnneer als Godt zijn strengh gericht -Ten jonghsten dage komt te houwen, -Soo sal sijn volck, elck als een licht, -Ia Son zijn, in haer glans 't aenschouwen: -Maer het boos geslacht, -Dat haer heeft veracht, -Sal sien, wat haer dan wordt toegebracht. -2. Als sy dan dus Godts kind'ren sien, -Sal d' eene tot den ander spreken: -Ach wee ons doch! Wel wie is dien -Die daer komt als een Son voort breken? -Wy en dachte noyt, -Dat die gene oyt, -Dus heerlijck soude werden opgetoyt. -3. Wy achten haer, elck voor een dwaes, -En deden niet als haer bespotten: - -En nu zijn sy Godts volck Helaes: -Maer wy zijn nu verdoemde sotten, -Sy genieten rust, -Eeuwigh soete lust, -Terwijl dat ons vuur noyt wort uyt geblust. -4. Nu siene wy het klaerlijck aen, -Dat het werkeerde wegen waren, -Op't welcke dat wy zijn gegaen, -En daer door, aldus zijn gevaren. -Wy hebben van 't quaedt, -Ons eertijdts versaedt, -Daer door zijn wy in een verdoemde staet. -5. Mochten wy weer van vooren aen, -Des werelts-dal eens gaen betreden, -Wy souden beter wegen gaen, -En nutter onse tijdt besteden: - -Maer 't is al geschiedt, -Klagen helpt nu niet, -Ons rest niet anders als eeuwigh verdriet. -Besluyt. -6. Wy nu, die Godt noch vrind'lijck noot, -En stelt sijn genaden-deur open, -Laet ons niet op den wegh ter doodt, -Maer op den wegh des levens loopen, -Ende houden Godt -Voor ons deel, en lot, -En schicken ons leven na sijn gebodt. -
-
-
- -Cupido's Fabel bevind ick vals. -
-Stem: O schoone die my dus mart'liseert? - -EEn oude Fabel, van lange wijl, -Stelt ons Cupido voor, -Dat hy met sijne boogh, ende pijl, -Des Minnaers hert schiet door, -Waer uyt terstont de minne swelt, -En maeckt dat hy sijn liefd' op yemant stelt. -2. Men stelt hem met een fackel in d' handt, -Gevult met brandent licht, -Daer door hy terstont in 't ingewant -Een Minne vlamme sticht: -Sy schild'ren hem oock als een kindt, -Sonder gesicht, en beyde oogen blindt -3. Die dit versierde seer grootlijcks loogh, -En die 't gelooft is mal: -Een kindt, een fackel, een pijl, een boogh, -Die doen hier niet metal: - -De Minne vlam, die soete brandt, -Die comt ons toe, van heel een ander kant. -4. De minne die sijnen scherpen schicht -My tot de ziel schoot in, -Quaem aen gevlogen uyt het gesicht -Van haer, die ick bemin: -Haer soet, en minnelijcken oogh, -Was 't altemael, de pees, de pijl en boogh. -5. De fackel die my in mijne jeught -Het herte stack in brandt, -Dat was haer reyne en witte deught, -Haer zeden, en verstant: -En daer quam oock haer schoonheydt by, -Waer by Heleen' niet te gelijcken zy. -6. Maer Cupido die en wasser niet, -Hy heeft my noyt geplaeght, - -Maer die mijn hart heeft in haer gebiet, -Dat is een jonge maeght: -Ick swijgh wie 't is: maer segge so, -mijn liefste in nu selver Cupido. -
-
-
- -Valsche Vrintschup. -
-Stemme: Ick min mijn Herder. -DE valsche vrinde, -Die ons beminde -Om dat ons sake -Haer konde vermake, -Die neme strack de vlucht, -Wanneer ons treffen, druck, en ongenucht. -2. My dunckt sy icone - -Seer fraey, en schoone, -Dronckaerts maniere, -Die sitten te viere -Soo langh men tapt goe dranck: -Maer schenckt men haer hef, sy gane haer gank. -3. Dit is oock even -En na het leven -Gelijck als doene -De bladeren groene, -Die proncken den Boom schoon -Somers, maer 's Winters ontvallen sijn kroon. -4. Oock als de swalen, -Die in de salen -Ons soet toe singen, -Als ons alle dingen -Toe lacchen met genucht, - -Maer 't Winters dan trecken sy op de vlucht. -5. Is yemandt druckigh, -Hy is geluckigh, -Soo hy heeft vrinden, -Die hem altijdt dienden: -Maer 't is rijckst die leeft, -Die gene vrienden van nooden en heeft. -
-
-
- -Bruylofs-Liedt. -
-Stem: De May die comt ons by seer bly. -GEluck, ghy die de tijdt van strijdt -In 't droevigh vryen uyt-gestreden hebt, -En met malkaer treet in 't begin - -Van soo een staet daer in ghy lusten scheyt, -Daer in dat ziel, en hert, -T' Samen gesmolten werdt, -Door vlamme van liefde, en minne-brandt. -Geluckigh Iongh-paer, -Nu vindt u te gaer -Een soeten bandt. -2. Men vint geen meerder soet, noch goedt, -Dat in de wereldt yemandt soo behaeght, -Als dan een minnaers hert, nu werdt -In eygendom aen sijn geliede maeght. -O soete lieve min, -Wat hebje vreughden in? -O haven van ruste! O stille Ree -Daer den Bruydegom -Pluckt de maeghde-blom, - -In lust, en vree. -3. Daer maeckt de Bruydt terstont gesont, -Haer minnaers hart, dat sy verwondet heeft. -Soo wanneer sy uyt lust hem kust, -Is 't ofje hem een nieuwen leven geeft. -Wel saligh is de lust, -Die aldus wordt gheblust: -Waer liefde en trouwe te samen gaen, -Wordt de eerste wet -Van Godt in geset -Als dan gedaen. -4. Ghy meysjes, treedt wel dra haer na, -En plaeght u trouwe minnaers langer niet, -Wiens herte dat ghy steelt, maer heelt -Haer droeve smerten, en haer groot verdriet, -Dees Bruydt die gaet u voor, - -Treedt ghy oock in haer spoor, -En volget de gangen, die sy nu gaet. -Lust, en leckerheydt, -Is voor u bereydt -In d' Echte staet. -
-
- - Lof sangh van de Vliet. -
-Stem: Voorby is 't Winters herde stoet. -HEf op, mijn sangh-Godin, een Liet, -En met een soet gheschater, -Singh eens ter eere van de Vliet, -En van haer suyver water. -2. Het is een Bron vol soet genucht, - -Sy speelt musijck met suysen, -Soo wanneer als de Somer lucht -Haer gollefjes doet bruysen. -3. Dit ruyschende, en soet geluyt, -Door 't roeren van haer stroomen, -Dat lock de Steedsche jeughde uyt, -Om daer mee by te komen. -4. Die van laveeren dwers, en langhs, -Met vlaggen op haer Schuytje, -Wt 't welck men hoort veel soet gesanghs -En 't spelen op het Fluytje -5. De Knolle-vletter uyt de streeck, -Hoort men hier mede singen: -'t Vergadert al op dese Beeck, -Dat vreughde kan aen bringen. -6. Den Huys-man in sijn melleck-schuyt, - -Voeght hem by de Besanen, -En singende met soet geluyt, -Ontreckt de Zee haer Swanen: -7. Die latende de bracke Zee -En hare dorre stranden, -Beproncken nu de Vliet oock mee, -En al die naeste landen -8. De Koeten houden in het Riet, -En duysent and're mede: -Schier al wat men ter wereldt siet, -Versamelt daer ter stede! -9. De grondt die wimmelter van Vis, -(Het schijne wel mirakels,) -Die met vermaeck te vangen is, -Met Elgers, of met Schakels. -10. Of sooje maer een Fuyckjen set, - -Ter zijden voor een Slootje, -Slaept vry gerustelijck op u bedt, -Des morgens hebje 't sootje. -11. Des morgens legger by malkaer, -De Korpers, Baers, en Snoecken: -Men vanght oock licht een zood te gaer, -Met Angels, en met Hoecken. -12. Wanneer des winters koude Vorst -Ons knelt op onse rugge, -Soo stremt hy op de Vliet een korst, -Een held're glasen brugge. -13. Wat isser dan al reeds, en ganghs, -De Peertjes, wack're Beesies, -Die draven dan de Vliet al langhs, -Met duysent andere Sleesies. -14. Gewis, het is een soet vermaeck, - -By dese Vliet te woonen: -Hy self heeft kennis van dees saeck, -Die u dit doet vertoonen. -
-
-
- -Seven personaedjen. Een Huys-man singht. -
-Stemme: Wie wil hooren singen. -DAer is geen staet te noemen, -Al schijnt een Huys-man slecht, -Dat reden heeft te roemen -Van sulcken ouden recht: -Want Kain, en Abel beyde, - -Die gingen op de weyde -En volghden die manier, -Als nu yder Huys-man hier. -2. Abram dat was een Herder, -Den vromen Iacob mee: -En komen wy wat verder, -'t Is al te doen met Vee: -Iob vulde met Schapen, en Ossen, -De Velden, en de Bossen: -Saul een kroone droegh, -En dreef even wel den ploegh. -3. Wat was 't dat haer dus maeckte -In desen staet gerust? -'t Was om dat sy dus smaeckte -Een leven vol van lust, -En datse dus niet en wisten, - -Van lagen, noch van listen, -Maer hadden al het soet, -In een vollen overvloedt. -4. Daer hoeft gheen gelt verspilt, -Gaet maer een weynigh delven, -Soo hebje, watje wilt: -Wy plucken al uyt den grase, -De Boter, melck, en Kase. -Den Acker deelt ons by, -Koorn tot meel, en leck're Bry. -5. Men weet van geen slampampen, -Daer na de Brasser hijght, -Die hondert duysen rampen, -Tot loon voor 't brassen krijght. -Wij soecken geen ydele eere, - -Men acht geen zijden kleere, -Ons vrolijck herte lacht -Om des wereldts malle pracht. -6. Want 't zijn veel beter dingen -Daer op den Huys-man lonckt, -De Vogeltjes die singen, -En 't veldt seer cierlijck pronckt: -Hy siet 'er het water stroomen, -En vruchten aen de Boomen, -En menigh schoonen Geest, -Dan onspringht hem sijnen geest. -7. Maer soo ick alle dingen, -Sou' trecken in mijn sangh, -Die ons vermaeck aen-bringen, -Ick song ses dagen langh: -Ia lichtelijck den Sondagh mede, - -Welk ick niet geern, en dede -Dies segh ick voor het slot: -O! geluckigh Huys-mans lot. -
-
-
- -Een Stee-man singht. -
-Stemme: Phylis komt u buygen. -WIlie u vermaken, -Met verscheyden saken? -Wilie eenen staet -Sien, soo soet als heunich raet? -Komt dan onse Stad genaken, -Ende wandelt op ons straet. -2. Alles wat de Velden - -Den Huys-man bestelden, -Sijnen melck, en Room, -Komen met een snellen stroom, -Alles wat men hoordet melden, -Komt daer, en 't is wellekoom. -3. Daer sulje aenschouwen, -Huysen, en Gebouwen, -So schoon dat het oogh -Stadigh steygert na om hoogh: -Ghy sult nau u oogh betrouwen, -Want 't is of het u bedroogh. -4. Daer sulie aenmercken, -Hondert Ambachts wercken, -Alderley Fatsoen, -Daer sy 't Huys gesin door voen: -En 't gemeene best verstercken, - -Want elck heeft haer werck van doen. -5. Wat men kan begeeren, -Eten, drincken, kleeren, -En meer onder goedt, -Dat een yder hebben moet, -Ende niemandt magh ontbeeren, -Vindt ghy daer in overvloet; -6. Daer zijn hooge wallen, -(Noodigh boven allen -Tot des wereldts nut, -Die bepland staen met geschut, -'t Welck des Vyandts overvallen, -In haer dullen fury stut -7. Maer soud' ick uyt-singen -Alle goede dingen, -Die een geestigh man, - -Van de Stadt wel seggen kan, -Ick soud' hals, en keel verwringhen, -'t Is dan best, ick scheyder van. -
-
-
- -Een Zee-man singht. -
-Stem: Hoort toe matroosen al te saem. -EN roemt niet van u Landt, noch Stadt, -Noch van u kost'lijck Vee, -Want al de rijckdom, en de schat, -Die komt u uyt der Zee: -Ick noem met bescheydt -De Schip-vaert 't wereldts hansen, - -Daer sy haer goedt mee leyt, -Van dees, in gene Landen. -2. 't Rijck Indien ontsluyt sijn mijn, -En levert vele Gout: -En Spanjen schenckt ons soete wijn, -En Vranckrijck geeft ons Sout: -Van elders komt ons Glas, -En Koper, Tin, en Verwen, -En Pick, en Teer, en Vlas, -En Roggen, ende Terwen. -3. Den Noor-man hackt gestaegh in 't Wout -De Boomen onder voet, -Niet voor hem self: maer dat hy t' hout -Na Hollandt senden doet. -Denckt nu in u verstant, -Wat raedt om dese dingen, - -Van 't een in 't ander Landt, -En over zee te bringen, -4. Hier weet een Boots-man daetlijck raet, -En komt in dit geval, -Met Schepen daer hy het in laedt, -En steeckt weer van de wal, -Om 't grondeloose ruym -In haest te overvaren, -Terwijl hy peeckel-schuym -Brouwt, in de soute baren. -5. Als dan soo een geladen vloot -Komt in de Steden aen, -Al was de Neeringh daer in doodt, -Sy soud' uyt 't Graf op staen. -En yder een ontfonckt, -En vindt dan sijn begeeren. - -En 't gantsche Landt dat pronckt -Gelijck in Bruylofts kleeren. -6. Maer soo de Zee-vaert wat vervalt -Het rijcke Landt ver-ermt, -De Stadt verliest strack sijn gestalt, -En yder Borger kermt: -Maer als een volle vloot -Komt zeylen in de Haven, -Krijght elck weer in de schoot, -Duysent, en duysent gaven. -7. Maer indien dat ick in't geheel, -Des Zee-vaerts nut uyt-songh, -Ick hoefde wel een yseren keel, -En een metalen tongh: -Maer 't is de pijn niet weert, -Die in mijn hooft te bringen: - -Soo yemandt meer begeert, -Die magh self meerder singen. -
-
-
- -Een Koop-man singht. -
-Stem: Ick hoor aen dees vogelen singen. -WIlje weten wat de Neeringh -Aen de Stadt, en Landen schenckt? -Wat de welvaert, en verkeeringh -In de rijcke plaetsen brenght? -Ick segh dat des Koopmans handt, -In de Stadt, en in het Landt, -Al de schat, en rijckdom plant. -2. Schoon den Huys-man in de velde, - -Tot verwonderingh van elck, -Hondert Koeyen t' samen telde, -Yder een Fonteyn van melck, -Soo hy daer van niet verkocht, -Was hy in een staet gebrocht, -Daer hy niet in leven mocht. -3. Schoon de Stad met volle Salen -Op-gepropt van alle dingh, -Trots, en heerlijck staet te pralen, -Soo is 't noch, wat sy ontfingh, -'t Zy van schatten, ende gelt, -'t Zy van sterckheydt, en gewelt, -'t Is door ons daer in bestelt. -4. Schoon een vloot van duysent Schepen, -Na de vaert, en wel-vaert tracht, -Om veel waren in de slepen, - -Soo geen koop-man haer bevracht, -Blijft de waer in't selve Landt, -Daer 't den Schipper eerstmael vant: -En de vloot vertreckt met schant. -5. Schoon een vloot oock quam gevaren, -Van een verre, rijcke kust, -En hier brachte duysent waren, -Tot een yders vreught, en lust, -Kochte niemandt van die last, -Het bleef leggen by de mast: -Denck eens hoe of dat wel past. -6. Maer nu, wat de vreemde Landen, -Ons ontsluyten uyt haer schoot, -Goudt, en Sout, en alderhande -Dingen, noodigh in de noot, -Koopt den koop-man hier by een, - -Stapelt het in onse Steen, -Ende maeckt het elck gemeen. -7. Alsoo wordt door 's koop-mans handen, -Al het kostelijcke goedt, -Hier gebracht in onse Landen, -In een vollen overvloet. -Maer hield' ick met singen aen, -'k Song my dat ick niet mocht gaen -Daerom laet ick 't singen staen. -
-
-
- -Een Krijghs-man singt -
-Stem: Een Ruytertjen jongh van jaren -INdienje eens willet letten -Hoe 't in de Wereldt gaet, - -Wat stijle men dient te setten, -Tot steunsel van yeder staet, -Gy sullet bevinden, dat Burger, noch Boer, -Noch koopman noch zeman, kan dragen op schoer -Des werelts welstant: maer dat het moet doen -Den Krijghs-man, met Degen en Roer. -2. Den Huys-man seer rijck, en prachtigh -En wel versien met gheldt, -Die isser nochtans niet machtigh, -Te weeren het wreedt gewelt: -Als komen de stroopers, en rooven sijn goedt, -Verliest hy sijn rijckdom, en mede de moedt. -Maer door den krijghs-man wel veylig bewaert -Soo verkrijght hy weer overvloedt. -3. De Steden alwaer vergaren, -Schatten van Landt, en Zee, - -Wel soude haer oock bewaren, -Indien het geen Krijghs-man dee? -Wanneer der een Leger de muuren beklam, -Of schoonder een Backer, of wever aen quam, -Och laci! wat was 't? sy souden van vrees, -Wel worden kreupel, en lam. -4. Schoon Zeeman al meent te zeylen, -Na meenich vreemden Landt, -Seer lichtlijck kan het hem feylen, -Soo dat hy wordt aengerant, -Van Roovers, en schuymers, en stroopers op zee: -Maer voert hy krijghs-helden, en wapenen mee, -Soo strooyt hy de stroopers: en vaert al voort, -In rusten, ende in vree. -5. Den Koop-man, die sijne goeden, -Doet stuuren wijt en breedt, - -Laet zijne Waren behoeden, -Van Helden met stael bekleedt: -Dit siene de Roovers, en neme de vlucht, -Want yder die isser voor 't leven beducht: -Dus come de ware des koop-mans aen, -Tot yders vreught, en genucht. -6. Wy binden met stalen banden, -Den wrevel, en 't gewelt, -En brengen de Dwinge landen -Geknevelt al uyt het velt: -Wy winnen de machtige steden van 't Landt, -En strooyen het ciersel van onsen Vyandt, -En voeren het in triumphe mee, -En planten het in ons Landt -7. Maer soud' ick al de profijten -Van d' Oorlogh singen uyt, - -Ick soude veel tijdt verslijten -En 't singen en geeft geen buyt: -Ick soude soo lange wel singen aen een, -Dat 't Leger wel soude innemen drie Steen, -En was ick daer niet dat speet my seer, -Adieu dan: want ick treck weer heen. -
-
-
- -Een Medecijn-meester singht. -
-Stem: Seght mijn schoon Godinne. -VRaeghje wie het meeste goedt -Aen het Landt, en Steden doet? -Sonder lang beraden -Segh ick, dat een Meesters handt, - -Aen de Steden, en het Landt, -Doet de-nutste daden. -2. Daer en wordt geen mensch verschoont, -Rijck, noch arm, noch waer hy woont, -In de Stadt, of dorpen, -Of hy wordt licht kranck, en swack, -En is pijn, en ongemack -Stadigh onderworpen. -3. Als haer dan een sieckt verheft, -Als haer pijn, en lijden treft, -Door de gantsche leden, -Wat baet dan des Huys-mans Vee? -Wat baet dan de grijse Zee? -Wat baet dan de Steden? -4. Dan sal al des koop-mans goedt, -En sijn rijcken overvloedt, - -Hem niet heylsam wesen: -Dan ist met den Krijghs-man uyt, -Spiets, noch Sabel, roof noch buyt, -Can hem niet genesen. -5. Maer de Heere heeft het kruyt, -Dat hier uyt der aerde spruyt, -Gemaeckt een geneester -Van veel lijden, en ellent, -En maeckt hare kracht bekent -Aen een schrander meester. -6. Die dan stelt sijn kuur te werck, -Daer voor dat hy menigh sterck, -En gesont doet maken, -De dus door des meesters handt, -Blijven in een goede stant, -Al des wereldts saken. - -7. Maer soud' ick nu singen uyt, -Wat door Salf, door Smeer, en kruyt, -Wy al voordeel krijgen, -Ick soud singen alsoo langh, -Dat ick self word' sieck en bangh, -Daerom wil ick swijgen. -
-
-
- -Een Predicant singht. -
-Stem: O schoon Kariclea -EEn mensche van natuur, -Is wel bequaem om yder Landt, en Stadt, -Door neerstelijck bestuur, -Te doen vervullen, met rijckdom, en schat, - -Maer kan niet vaten in sijn gemoet, -Hoe men moet winnen het hooghste goet. -2. En wat comt hem te baet, -Dat hy al in 't besit verkregen heeft, -Rijckdom, en eer, en staet, -Die hier de wereldt aen haer beminders geeft, -Soo hy de wereldt voor d' Hemel kiest, -En dan ten lesten die beyde verliest? -3. Wat is 't, of yemandt zeylt -Tot aen het ende van den Oceaen? -En alle gronden peylt, -En weet de werelt geheel romdom te gaen? -Soo hy met enen oock niet verstaet, -Wat wegh ten Hemel al binnen gaet? -4. En of een Krijghs-man oock -Door hem de gantsche werelt beven doet, - -En blaest staegh vuur, en roock: -Indien hy selven in sijn verwoest gemoet, -Sijn eygen driften niet en bevecht, -Soo blijft hy dan noch een Satans knecht. -5. Dies is in Landt, noch Stadt, -Schohon datse zijn vervult met machtigh goedt, -Geen kostelijcker schat, -Als 't woordt dat Godt ons nu heden schencken, doet, -En dan den Hemel alhier gestiert, -Ons Landt daer mede op't hooghst verciert. -6. Het wijst ons 't hooghste goedt, -En oock den wegh, op welcke men daer gaet: -En oock hoe yder moet -Hem dragen in sijnen wereldlijcken staet: -En wat voor plichten in elck geval, -Hy aen de menschen, en Godt, doen sal. - -7. Wilt ghy van nu aen voort, -Den zegen op ons Landen trecken neer, -Soo draeght u na het woordt, -Dat van den Hemel afcomt, van Godt de Heer. -Dan wordt de wereldt zegen bereydt, -En voor u selven de saligheydt. -
-
-
- -Nu zijn de Personadjen uyt, Dies singh ick hier op tot besluyt -
-Stem: Maximilianus de Bossouw. -WIe wel bemerckt de ordeningh -Van al der menschen wercken, - -Die sal een soet en seltsaem dingh, -In dat gesicht bemercken, -Te weten hoe den een, en d'aer -Te samen spannen met malkaer, -De wereldt te verstercken. -2. Niemandt die 't al uytvoeren can, -Dat wel gedaen moet wesen, -Nu vaten wy't te samen an, -En alsoo wordt door desen, -Het Landt met al sijn rijcke Steen: -Ia heel de Wereldt in 't gemeen, -Behouden in haer wesen. -3. Dus wordt de werelt seer bequaem, -Van hoogh-geleerde Mannen, -Geleken by soo een lichaem, -Wiens leden t'samen spannen, - -Om elck te doen soo veel het can, -Waer door dat oock het onheyl dan -Verjaeght wordt, en verbannen. -4. Het ooge licht de voeten voor -De voet die draeght de handen: -Het herte luystert door het oor, -De buyck leeft door de tanden, -En alsoo voort met al de rest. -Soo is 't oock in't gemeene best, -Van Steden, en van landen. -5. Een yder blijf in sijn gelit, -En doe sijn eygen saecken, -Soo elck dit doet, soo sal oock dit, -Des wereldts welstant maken: -En't Landt in sulck accoort gestelt, -Is niet te winnen door ghewelt: - -Want 't harnast alle saken. -
-
-
- - - - - -Rou-Klacht: Over de doodt van den Admirael Tromp: Geschoten den thienden Augusti 1653. -
-Stem: Mijne Harp bekleet met rouwe. -DRaeght nu vry geen swarte rocken, -Als men eertijdts placht: -Want het Landt is self betrocken - -Met een swarte nacht: -Eenen Son, tot 's wereldts wonder -Van elck een beschout, -Doock onlanghs in 't westen onder, -En soomd' het met goudt -2. Eenen muur, in 't rond gelegen -Om ons Vaderlandt, -Is nu onder voet gheslegen, -Door des vyandts handt. -Eenen onversaeghden Krijger, -Waer voor Spanjen vloot, -Meer als voor een Leeuw, of Tyger, -Laci! die is doodt. -3.Tromp, die is nu doodt geschoten, -En ter neer gevelt, -Nu is 't edel bloedt vergoten, - -Van dien dapp'ren Heldt: -Yder Burger hoort men weenen, -Treurigh, en bedroeft: -'t Gantsche Landt dat is met eenen -'t Herte toe-geschroft: -4. 't Bloedt verstremt my in sijn ader -Door de konde schrick, -Om dat Hollandts Vrede-vader -Is een leefloos Lijck. -Wie sal nu weer uyt Kartouwen, -Swanger met het Kruydt, -Donder, ende Blixem spouwen, -Met een groot geluyt? -5. Wie soud' lusten nu te schieten: -Laet den kanon staen: -En laet uyt u oogen vlieten, - -Eenen Zee van traen. -Draeght nu vry geen swarte rocken, -Als men voortijdts placht: -Want het Landt is self betrocken, -Met een swarte nacht. -
-
-
- -Lof sangh van een goede Vrouwe. -
-Stemme: Mijn alderliefste verheven. -JCk voel mijn geest gedreven, -En aen-geprickelt stijf, -Om een Lof-sangh te geven -Aen een Godtsaligh wijf, -En aen haer vrom bedrijf. - -Om dit dan te beginnen, -Met in-getogen sinnen -Is 't, dat ick aldus schrijf. -2. Een Vrou begaeft met deughde, -Is als een sonne-schijn, -Die 't al verlicht in vreughde, -Alwaer haer stralen zijn: -Sy is een medicijn, -Die met haer lieflijck wesen, -Haer liefste doet genesen, -Van druck, verdriet, en pijn. -3. Sy is een stille haven, -En ruste voor de jeught: -Sy doet haer weergaed laven -Met soete lust, en vreught, -Daer door hem 't hert verheught: - -Het doet sijn geest verstercken, -Wanneer hy gaet bemercken, -Haer wijsheydt, en haer deught. -4. Sy is gelijck een wingert, -Vol druyven door malkaer. -Als sy haer arme slingert -Om haren weder paer, -Wanneer hy flau[...]r en naer. -In jammer soud' versticken, -Soo doetse hem verquicken, -En maeckt een vrolijck paer. -5. Sy is een goude kroone, -Die op het hooft des mans, -Pronckt cierelijck, en schoone, -Meer als een Peerlen krans: -Haer deught is Hemels glans: - -Sy is een schat van minne, -En oock haer Huys-gesinne -Een onvervinb're schans -6. Sy is een Hemels zegen, -Een gave van de Heer -Maer ick singh noyt ter degen -Haer wel verdiende eer: -Want sy verdient veel meer. -Wel hem, die soo een Vrouwe -Gheschoncken wordt in trouwe, -Door gunste van den Heer. -
-
-
- -De snelligheydt des tijdts. -
-Stem: Lestmael ging ick op eenen morgen. - -GElijck een Vogel die daer henen -Vlieght, met een wack'ren snellen vlucht: -Of als een mist, in haest verdwenen -Voor 't aenschijn van de blauwe lucht, -Soo is oock even -'t Loopen van den Tijdt, -En van ons leven, -'t Zy men is verblijdt, -Of droeven smerten lijdt. -2. De nacht die kringht den dagh van stede, -Soo doet den dagh de nacht oock mee, -De Winter, en de Somer mede -Die drijven oock malkaer van 't stee -En al vreughde -Van des wereldts soet: -De groene jeughde, - -En haer bly gemoedt, -Vergaen gelijck een vloedt. -3. Schoon yemant op den troon geklommen -De wereldt had in sijn bedwangh -Noch stut hy niet, door groote sommen -Van yseren volck, de tijdt haer gangh: -Ia self sy allen -Sonden door de tijdt -Haest neder-vallen; -Want het is een strijdt -Die't alles neder smijt. -4. Dit wordt terstont ons voor-ghedragen, -Soo haest men in den Bijbel leest, -Daer staet van de ses eerste dagen, -'t Is Avondt, en morgen geweest: -Om dat de saken - -Des wereldts, in 't kort -Een eynde maken, -Gelijck den dagh wordt -In duysternis ghestort. -5. Alleen wordt d' Avondt niet beschreven, -Op 't lest van den sevensten dagh, -Om dat ons af-beeldt het leven, -Dat noyt de tijdt af-snijden magh. -Bouwt dan u sinnen -Niet op ydelheydt: -Maer wilt beminnen -'t Geen om hooge leydt, -'t Welck blijft in eeuwigheydt. -
-
-
- - -Ken-tekenen van een Kindt Gods. Psalm 15. ende 24. Iesai.33.3 -
-Stem: Ay schoonste Nimph? aensiet, &c. -WIe ist die vry, met ongeboeyde ziele, -Bevrijdt van alle kruys, -Om hoogh sal gaen, als op Elias wielen -Tot boven in Godts huys? -En hoe moet hy, en sijn gestalte wesen, -Die voor Godts toorn ('t welck is een gloet, -Die al de boose gants verdoet) -Niet hoeft te vreesen? -2. Het is die geen die ongewoon tot liegen, - -Geen vals geruchte smeet, -Die niet een mensch sal wetende bedriegen, -Of oorsaeck zijn tot leet: -Die t' aller tijdt de kind'ren Godts doet eeren, -Maer 't boose volck dat Godt versmaet -Is vyandt door een heyl'ge haet: - En haet vals sweeren. -3. Die om gewin, door vuyle boose stucken -Sijn naesten niet uytsuyght, -Noch die oock niet, om d' arme t' onder-drucken -De Rechten omme-buyght. -Die door geschenck, noch oock door boose leere, -De suyv're waerheydt niet verlaet, -Noch oock het goede om het quaedt, -Niet doet verkeeren. -4. Die sijn gemoedt voor Gode kan uytstorten, - -En toont een suyver hert: -Die door bedrogh, oock niet en sal verkorten -Sijn schulde, tot yemandts smert. -Die oock in tucht alsoo bedwinght sijn oogen, -Dat sy niet leyden tot het quaedt: -Die door des wereldts schijn-cieraert, -Niet werdt bedrogen. -5. Siet die is het, die daer sal gaen om hoogen -En eeuwigh aldaer zijn, -En alle heyl geniet voor Godes oogen, -En salige aenschijn: -De Hemel sal zijn stercke vestingh wesen, -Daer hy met zegen overstort, -In eeuwigheydt behouden wordt -Sonder te vreesen. -
-
-
- - -De voorspoedt der Godloosen. Psalm 73:2. 18. -
-Stemme: Granida Princesse. -ICk was schier geweken -Van de rechte paden, -En verdwaelt door enckel ongedult, -En door nijdt ontsteken, -Doe ick sagh de quaden -Met rijckdom, in voorspoet opgevult: -Sy schricken noyt in doodts gevaer een reys, -Maer staen soo vast gelijck een sterck Paleys. -2. Geene ongelucken, - -Noch geen harde slagen, -Nu gemeen by 't menschelijck geslachte, -Komen haer oyt drucken. -Heeft dan Godt behagen -In haer quaedt, en in haer trotse pracht? -In wulpsche weeld' mest elck van haer hem vet, -'t Wijl hy Godts volck met lasteren besmet. -3. Als oock sulck een harer -Sijn lippen doet open, -Is 't of sijn stem van boven comt of, -Dan komter als water -Het volck aen geloopen, -En offert aen hem seer grooten lof, -En prijst seer hoog, sijn hooghmoet, en sijn trots, -En vloeckt met een de lieve kind'ren Godts. -4. Siet dat is ter degen - -'t Fatsoen af geschildert, -Ende haren aert gecontarfeyt: -Maer ick daer en tegen, -En ben noyt verwildert, -En heb na Godts Wet mijn koers geleydt, -Maer wordt geplaeght, alwaer ick my begeef: -Is 't dan vergeefs, dat ick onstraflijck leef? -5. Siet in sulcke reden -Woud' ick schier uytvaren, -En had oock by na haer seer genoemt: -Maer dan had ick heden -Godts volck die oyt waren, -En nu zijn behouden, heel verdomt. -Ick dachte oock: wel waer comt dit van daen? -En kond' dees saeck ten degen niet verstaen. -6. Tot dat ick gingh treden - -Op het alderleste, -In Godes Huys, en in sijn Heylighdom, -Daer vondt ick de reden, -Waerom sy soo meste -Sadt en vet: de reden is, daerom, -Godt heftse hoogh, eer heyse neder-stort, -Op dat haer val alsoo te grooter wordt. -
-
-
- -De bekeerde Moordenaer, Luc. 23. -
-Stemme: De Herdertjes in de nacht. -EEn moordenaer die altoos // Goddeloos -Vermoorde, en verscheurde, gelijck een Beer - -Die nimmer niet hoord -Van Christus, noch sijn woordt, -Die noemde aen 't Kruyce, Iesus sijn Heer. -t' Wijl Petrus in de Zael -Hem loochende driemael, -Hem Iudas verrade: Hy selver met pijn -Beladen soo seere, -Riep: Waerom, Ach mijn Heere, -Verlaet ghy nu mijn? -2. t' Wijl yder in dit verdriet // hem verliet, -En vluchte eer hy was aen 't Kruyce gehecht, -Soo datter gants geen blijck -Was, van zijn Koninghrijck, -Sagh nochtans dees moorder sijn Konings recht -t' Wijl een Apostel hingh -En moorders loon ontfingh, - -Soo worde een moorder een Euangelist: -Die onlanghs seer sondight, -Nu Christi lof verkondight: -Wie had dat gegist? -3. Ia selver dees moordenaer // was voorwaer -Veel wijser als d' Apostels alle gelijck, -Sy vraeghden noch daer na -Sult ghy, o Heere dra -Aen Israel herstellen het koninghrijck? -Sy sagen Iesus aen, -Voor een, die soud' verslaen -De Roomsche Tyrannen, en storten haer neer. -Maer desen in't sterven -Die bidt des Hemels erven -Van des Hemels Heer. -4. Hoe konde dees mordenaer // also klaer - -Sien, door dees dicke wolcke, en nevelen heen, -Dat hy in dit ellent, -De Godtheydt Iesu kent, -Daer hy eenen leydts-man der moorders scheen: -Het is 't geloof geweest, -Gewrocht door Christi Gheest, -De welcke hem Iesus dus maeckte bekent: -Nu leeft hy hier boven, -Om sijn Heylandt te loven -Eeuwigh sonder endt. -
-
-
- -Vyanden der Kinderen Godts. -
-Stemme: Voorby is's winters harde stoot. - -VOor swellen die besloten zijn -Sal meer een meester vreesen, -Dan of alleenelijck de pijn, -Comt buyten aen te wesen. -2. Op ondiep Water, voor een Klip -Die loert in het verborgen, -Soo draeght een Stuurman voor het Schip, -De aldergrooste sorgen. -3. En een voorsichtigh Capiteyn, -Is voor verborgen lagen -Veel meer bevreest, dan of alleyn, -Maer wancken harde slagen. -4. Alsoo is oock 't verdorven vlees -De quaetst van ons vyanden, -En darom oorsaeck, dat ons dees -Bedecktlijck komt aenranden. - -5. Sy houdt gedurigh scherpe wacht. -En laet geen tijdt ontsluypen, -Maer neemt op ons gestadigh acht, -Ons wacht te onder-kruypen -6. Den Satan heeft met haer verdrach, -En loopt staegh om de kanten, -En soeckt waer hy sijn Standaert magh, -Op wal, en vestingh planten. -7. Het vlees onthout haer by de poort, -En soeckt hem in te leyden, -En tracht aen dees, of d'ander oort -Hem ingangh te bereyden. -8. Somtijts soo vint sy inder nacht, -Gants sonder sorge slapen, -Die daer gesielt zijn op de wacht: -Dan neemt sy haren wapen. - -9. En roepter strack den satan by, -En seydt: komt treet u binnen, -Want dese stadt, voor u, en my, -Die is nu licht te winnen. -10. Den satan valt in fury aen -En meent die stadt te rooven -Maer als hy binnen is gegaen, -Soo komt den Heldt van boven. -11. Den Hoeder Isr'els in de lucht, -Met sijn Geweer in handen, -En drijft den satan op de vlucht, -Soo dat hy ruymt met schanden. -12. Wel looft u Godt, na rechten eys -Ghy alle sijne kind'ren, -Die soo den satan, en het vleys, -Haer moed-wil doet verhind'ren. -
-
-
- - -Geestelijcke triumphe. -
-Stem: Hey hoe helder schijnt het maentje? -LOoft Godt alle ghy verloste -Van des satans slaverny: -Al uw' vyanden die moste. -V gaen laten los, en vry, -Doe quam // dat Lam, -Dat voor ons voldeed, -En Godts toorne voor ons leed: -Dat Lam // dat voor ons voldeed, -En steld Godt met ons te vreed. -2. Al des satans Helsche stricken, -Door ons sonden sterck in kracht, - -En de wet, die met verschricken, -Den sondaer voor 't oordeel bracht, -Zijn nu // voor u, -Ghy die Christi zijt, -Gants vernielt voor alle tijdt. -Voor u // ghy die Christi zijt, -Is geen schrick voor hel, of strijdt. -3. Dreyght de doodt u te verworgen, -Segh dan vry, O doodt! soo niet, -Eertijdts hebt ghy mijn Borge -Vast gehadt in u ghebiedt: -Maer hy // gingh vry, -Doe hy had betaelt, -En mijn schulden door gehaelt. -Maer ghy // doe hy had betaelt, -Hebt ghy niet als schand behaelt. - -4. Alle sijnse soo gevaren, -Sonde, Satan, Wet en vloeck, -Schoonse thienmael meerder waren, -Noch was Christus haer te kloeck: -Hy bracht // haer macht, -Gantschlijck onder voet, -Door sijn lijden, en sijn bloedt. -Haer macht // gantschlijck onder voet, -Maer ons schonck hy 't eeuwigh goedt. -5. Wat sal ick nu weder geven -Aen hem? schatten groot van som? -O neen! Ick wil self mijn leven -Schencken hem in eygendom: -Ick sal // het al, -Lijf, en Siele beyd, -Overgeven sijn beleyd. - -Ick sal // Lijf en Ziele beydt, -Schencken hem in eeuwigheydt. -
-
-
- -Toe-vlucht der Geloovigen. -
-Stemme: Polyphemus aen de strande. -JCk weet Heere, ghy sijt seecker, -Eenen wreecker, -En een straffer van het quaedt: -Wilt ghy dan in toorn verbolgen, -My vervolgen, -Soo en weet ick geenen raedt. -2. Schoon ick snelder als de veug'len, -Op de vleug'len - -Van de gulde dageraet, -Vloogh aen, t'eynde van het water, -Wiens geklater -Staegh de wreede Rotsen slaet: -3. Noch soud ick my self bedriegen, -En in 't vliegen, -Van uw' handt daer zijn bekayt: -Want ter wereldt niet en repter -Daer den Scepter -Vwer handt niet overswayt. -Schoon ick onder de Zee-golven -My bedolve, -En ick daer in't wel-sandt kroop, -Om soo in die diepe kuylen -V t' ontschuylen, -'k Was daer mee al buyten hoop. - -5. Schoon ick onder in der Hellen, -Socht te stellen, -Eenen Schuyl-plaets voor u oogh, -Noch souden uw' ooge stralen -Op my dalen -In der Hellen, van om hoogh. -6. Schoon ick mede soude derren -Bij de Sterren -Klimmen op een gouden stoel, -My soud' al het selve schorten, -Ghy soud' storten -My in d' onder-aerdtsche poel. -7. Schoon ick in de harde klippen -Konde slippen, -En haer zijn tot ingewant, -Daer my Son, noch maen, noch Winden, - -Konde vinden, -Soud' my vinden uwe handt. -8. Al het vluchten voor Godts tooren -Is verlooren, -Schoon ick was in 't stercktste slot -Datter is in 't Aertsch gewemel, -Onder d' Hemel, -Ick was daer noch naeckt voor Godt. -9. Sal ick dan niet angstigh beven -Ende geven -My de wan hoop tot een roof? -O neen! Ick wil noch al hopen, -En doen open -'t Scherp-siend' ooge van 't geloof. -10. Schuylende in Christi wonden, -Daer mijn sonden - -Voor Godts oordeel zijn bedeckt, -Ende wil mijn naeckte leden -Doen bekleeden -Met sijn deughden onbevleckt. -11. Laet dan vry Gods toorne blaken, -En doen kraken -Beyde Hemel, ende Aerd, -Evenwel sal ick dan wesen -Sonder vreesen, -En in Iesum wel bewaert. -12. Iacob dede my dit leeren, -Die met kleeren -Van sijn Broeder hem omhingh, -Ende alsoo quam voor Isack, -Aldaer hy strack -'s Vaders zegen door ontfingh. -
-
-
- - -Simsons Raedtsel, Iudic. 14:14. -
-Stemme: O saligh heyligh Bethlehem. -DAer quam van eenen eter spijs, -En van een stercken soetigheden. -Nu is de vraghe, op wat wijs -Moet men dit Raedtsel recht ontleden? -Antwoort. -2. By Timnath was een wreeden Leeuw -Een Pest, en plage voor de Velden, -Die staegh verweckt een moort-geschreeuw, -En 't Landt in een verbaestheydt stelde. -3. Een yder mensch en dorste schier -Dat gantsche Landt niet eens genaken: - -Want dat vervaerlijck Monster-dier -Verscheurd haer in sijn wreede kaken. -4. Maer Simson, welckets gantsche lijf -Met stalen zenuwen was door-regen, -Als hy verstont dit wreet bedrijf, -Voeld' hy sijn geest in hem bewegen. - 5. Eu stapte mee een rassche voet -Na Timnaths wout, dat Beest te stooren, -Daer hem dien Leeu terstont ontmoet, -Die strack vergramt in heete tooren. -6. En braeckt den Donder uyt sijn keel, -En schoot den Blixem uyt sijn oogen, -Niet anders, dan of hy geheel -De Helle hadde uyt-gespogen. -7. En quam met een geresen pruyck: -Met krullende bebloede locken, - -En meend' oock Simson in sijn buyck -Als and're lieden op te slocken. -8. Maer Simsom hief sijn handt eens op, -Die hy eerst eens rondomme swayden, -En trompt den Leeuw voor sijnen kop, -Dat hem sijn beyde oogen draeyden. -9. Daer stort dien Leeu, dat grousaem dier, -Dat soo op sijn krachten steunde, -En gaf een Brul, met sulck getier, -Dat al het Landt rondomme dreunde. -10. Simson met een metalen vuyst, -Die scheurd van een, de kop, en lenden: -Daer lagh dat Beest, met vuyl begruyst. -En Simson gaet hem elders wenden. -11. Maer als Simson daer na eens gaet -Voor by dees vuyle doode prye, - -Soo vondt hy daer in Honingh-raedt, -Daer in vergadert door de Byen. -12. Siet hier uyt neem ghy klaerlijck af, -Soo ghy met vlijt maer wilt aenmercken, -Hoe dat den eter spijse gaf, -En soetigheydt quaem van den stercken. -
-
-
- -De Mensche ellendigher als de Beeste. -
-Stem: Hoe legh ick hier in dees ellende. -O Soete jeught weest niet hooveerdigh, -Roemt niet op staet, of ed'len geest, -Want, schoon al schijnt een mensch seer weerdig, - -Noch is hy minder als een Beest: -Soo wanneer wy te recht bemercken, -Het aerdtsche lichaem, en sijn wercken. -2. De jonge Kalveren, en Lamtjes, -En al 't Gediert, die springen op, -En soeckens hares moeders mamtjes: -De Pilkes kruypen uyt de dop. -Maer een jongh Kindt leydt lange jaren, -En streckt sijn Ouders tot beswaren. -3. Als and're Dieren zijn gebooren, -En op de wereldt voort-gebracht, -Soo koom'se met haer kleedt te vooren, -Met pluymen, wol, of hayren vacht. -De mensch alleen heeft naeckte leden, -En and're moeten hem bekleeden. -4. Een Beest dat leeft oock sonder sorgen, - -Sonder arbeydt, en sonder sweet: -En 't sorght noyt voor den dagh van morgen, -Want altijdt staet sijn disch gereet. -Dit magh de mensche niet gebeuren, -Maer moet in 't werck hem schier verscheuren. -5. Natuur heeft oock een Beest geschapen -Bequaem tot tegenstant van strijt: -Want yder Beest dat draeght sijn wapen, -Daer mede dat het vecht, en smijt: -Maer d'arme mensch en draeght geen hoorens, -Geen klauwe, noch geen scherpe spoorens. -6. Een Beests veerdigh op sijn benen, -En afgeveerdigh tot de vlucht. -Een Vogeltjen dat vlieght daer henen, -Tot boven in de blauwe Lucht. -Maer een mensch is seer traegh van leden, - -En gaet heel log daer heene treden. -7. Een Beest leeft ook veel meer na reden -Als wel een mensche selver doet -Want het is in sijn staet te vreden, -Maer een mensch soeckt al meerder goedt. -En dit ontciert den mensch noch 't meeste, -Dat hy een dienaer is der beeste. -8. En wilt ghy van de kunsten roemen, -Die geschieden door 's menschen geest, -Soo sal ick u eens kunsten noemen, -Eerst uyt-gevonden van een beest. -En hy is van veel grooter eeren, -Die kunsten vindt, als diese leeren. -8. Wie heeft het mets'len eerst begonnen? -Het was een Swaeltien in sijn nest, -En hoe het Gaern moet zijn gesponnen, - -Leerden de spinnen alderbest. -En hoemen Landen moet Regeeren, -Dat mostmen van de Byen leeren. -10. En hoemen sijnen stem moet wringen, -In groven, en in fijnen tael, -Om alsoo goedt Musijck te singen, -Dat leerde ons de Nachtegael. -En oock de Aerde t' onder-mijnen, -Dat leerd ons Mollen, en Konijnen. -11. Een Pellicaen die leerd' het laten, -Door 't tappen van sijn eygen bloedt: -Een Oyevaer die leerd' ons vaten -Hoemen Klisteren setten moet, -Een Esel leerd' in oude tijden, -Datmen den Wijngaert moet besnijden. -12. En om door 't Roer een schip te stieren, - -Dat saghmen aen een Visch sijn steert. -In somma: daer zijn veel manieren, -Dat het Gediert ons heeft geleert. -Wat wil hem dan een mensch verheffen, -Daer hem de Beesten overtreffen. -
-
-
- -Een bekeerde Sondaresse, Luc. 7. -
-Stem: Ga wereldts minnaer vliet. -GHy die aen lust, en weelt, -Lijf, Ziel, en tijdt verspeelt, -Komt siet hier aen een aerdigh beeldt, -'t Welck was als ghy, maer neemt voortaen, -Voor 't Hels, een Hemels wesen aen. - -2. Komt leert een nutte les -Van eenen sondares, -Een vuyle Hoer: maer onder des, -Sy krijght berou, en wordt het moe, -En neemt haer gangh na Christum toe. -3. Daer sy een tranen vliet -Wt hare oogen schiet, -Dat niet uyt list, maer uyt verdriet: -Wt d' oogen, daer de geyle min, -Noch onlanghs hield haer woonplaets in. -4. Het Hayr dat als een net, -Tot vangen was geset, -En menighs kuysheydt had besmet, -Daer mee veeghd' sy de tranen af, -Die sy in 't schreyen over gaf. -5. De soeten Balsem mee, - -Daer voor sy eertijdts dee -Haer selven salven, doet haer wee, -Dies sy 't besteedt tot Christi eer, -En stort het op sijn voeten neer. -6. Haer mondt tot kus besteet, -In minne heyl, en heet, -Die kuste oock, maer 't was uyt leet, -Die kuste oock, maer 't was uyt lust, -Een lust die in haer Godt berust. -7. In somma: wat sy vint, -Dat dul, en onbesint -De ydelheden had bemint, -Dat wendse om, en gaf het weer, -Tot een geschencken aen haren Heer. -8. En hy, die sy dit geeft, -Is vriend'lijck, en beleeft, - -'t Welck hy haer oock bewesen heeft: -Want t'wijl sy drupp'len tranen laet, -Soo geeft hy stroomen van genaedt. -
-
-
- -Nieuwe-Jaers Liedt. -
-Stemme: Als 't begint. -HEt eerste daghjen in 't Nieuwe Iaer, -'t Welcke ons nu is verschenen, -Dat leert ons duydelijck, ende seer klaer, -Hoe snel ons leven gaet daer henen. -2. By dagen soo loopt het jaer ten ent, -By jaren verloopt het leven: -Wel dwaes is hy dan, die niet en bekent. - -Dat men het haest moet overgeven. -3. Maer wy die beter sijne geleert, -Wy vinden hier groote reden, -Nadien ons leven seer haestigh verkeert, -Om altijdt 't leven wel te besteden. -4. Seer menigh al in het jaer lest-le'en, -Swom oock in den poel der sonden, -De doot die ginger seer veerdigh mee heen, -En 't Graf heeft al haer vreught verslonden. -5. Wel wieje dan zijt, en laet u hert -Niet soecken wereldts genuchten: -Noch wilt oock mede in lijden en smert, -Niet al te seer, noch droevigh suchten. -6. Seer haestigh vergaet des wereldts soet, -En het suur, en bitter mede: -Maer leeft Godtsaligh, soeckt 't Hemelsche goet - -Dat blijft ons by, altijdt in vrede. -
-
-
- -Des Heeren Wijn-bergh Ies.5:1. -
-Stem: Tweede Carileen. -'K Wil de liefste die men siet, -Singen voor, van mijn bemind' een Liedt, -Die een Tuyn, en Wijn bergh, op den kruyn -Heeft geplant, -Van den vetsten heuvel in het Landt: -Wat steen hy vondt, in den grondt, is terstont -Wt geret, -Goede rancken ingeset: - -En een want, om den kant, vast geplant: -En hy toogh -In 't midden op, een toorn seer hoogh. -2. En hy maeckt een wijn-pars-back: -En hy groef, en dolf hem om, en stack, -Ia hy wrocht, hem ter deegh, maer hy brocht -Geen gewin, -Hy socht vruchten, maer daer quam niet in: -Wat dat hy sagh, dagh aen dagh, hy en magh -'t Smaken niet, -'t Geen sijn Wijn-bergh aen hem biet: -Van natuur, wrang en suur, als een muur, -Dor, en hert, -Is de vrucht, die bekomen werdt. -3. Komt Ierusalems Vier-schaer, -Vonnist nu, en spreckt in 't openbaer, - -Of een man, sijn Wijn-bergh, meer doen kan, -Als ick die. -Heb bewesen, en geen vrucht en sie, -Wel waerom schengt? waerom brengt? waerom dengt -Hy niet toe, -Vruchten aen my, wat ick doe? -Of ick ga, vroegh en spa, soecker na, -Iaren langh, -Wat ick vind, dat is suur, en wrangh. -4. Wel aen Wijn-bergh, het besluyt -Is gevelt: Ick sal het voeren uyt: -Sijnen wout, ruck ik om, dat de kant -Niet belet, -Noch kan schutten, wat hem geern vertredt. -Dan sal dien thuyn, op den kruyn, van dien duyn -Soo verciert, - -Zijn vertreden, van 't gediert? -Ia hy sal, heel en al, door dien val, -Oyt en oyt, -Zijn verwoest, ende heel beroyt. -5. Daer sal niet meer wassen uyt, -Als onreyn, en schadelijck onkruyt: -Nimmermeer, sal een wolck, aldaer neer, -Wt sijn vat, -Lossen sijnen Hemels water-schat. -Desen wijn-gaert, quaedt vermaert, quaet van aert -Quaedt van stant, -Is het Israels vette Landt: -En ick wacht, of 't geslacht, vreughten bracht -Na mijn sin, -Doch ick vind, daer maer sonden in. - -Hollandt, daer aen de Heere gaf, -In't eerst veel goedts in t' lest veel straf: -Het eerst uyt gunst, het lest na waerd' -Is even soo, als dees Wijngaerd. -
-
-
- -Des Heeren weldaden. -
-Stem: Geswinde Bode van de min: -GElijck Godt met sijn wijn-bergh dee, -In kana geplant, -Soo doet den goeden Godt oock mee, -Met ons Vaderlandt: -Want de Heer // heeft dit Landt, - -Tot sijn eer // door sijn handt -Met goede // vervult in overvloet. -Soo dat wijdt, en breedt, -Even als een kleedt, -Godts Barmhertigheydt, -Worde over 't Landt gepreyt. -2. In den Eersten quammer voort, -Inde duysterheydt, -'t Helder licht van Godes woordt, -Dat ten Hemel leydt, -Doe dit quam // uyt de lucht, -Daer op nam // strack de vlucht -'t Gespuys // en 't spoock van Babels huys. -Ende hier op strack, -Oock aen stucken brack, -Den handt, die de ziel - -Vast in Roomsche stricken hiel. -3. Doe al de wereldt dit aenschoud, -Maeckt sy haer bereydt, -Dat sy met Silver, en met Goud, -En meer kostlijckheydt -Vol van glans // die noyt dooft, -Eenen krans // op ons hooft, -Ten pronck // en tot verciersel schonck: -Yder Landt schonck wat, -Van sijn besten schat, -Alsoo wordt hier door -Holland, 's werelds schat-tresoor. -4. Het deense Bos, dat deel sijn hout -Hier aen Hollandt mee, -Daer van men duystent Schepen bouwt, -Swemmend op de Zee: - -Sulcken som // toe-gerust, -Seyld' romdom: yder kust -Die goot // sijn schatten uyt de schoot: -Want den Indiaen -Quam met silver aen, -Roodt gemenght met Gout. -Smanjen schonck haer wijn, en sout. -5. Vranckrijck dat gaf oock wat het kon: -Sweden koper sondt, -En al de Rogg, die Polen won, -Quam ons in de mondt: -Engelandt // pluysde sacht, -Met haer handt // 't Schaepjes vacht -Heel af // dat sy aen Hollandt gaf. -Somma, in het kort, -Yder Landt dat stort, - -'t Beste uyt sijn schoot -Yeder in de Hollandts' vloot. -6. En oock de Zee, seer grijs en blau, -Wt haer ingewandt, -Schonck Haringh, ende kabeljau. -En oock self Hollandt, -Word' een Hof // rijck in kruydt, -Alle stof // gaf het uyt, -En toond // dat Godes gunst daer woond' -Want daer vloeyd' een stroom, -Wit van melck, en Room, -'t Scheen dat yder Gras, -Een Fonteyn van suyvel was. -7. Doe nu dit door des Heeren gunst, -Hier quam by malkaer, -Soo quamen oock natuur, en kunst, - -Alle beyde daer, -Om dit Landt // in die staet, -Met haer handt // sijn cieraet -Heel kant // te stellen in sijn stant: -Dese maeckten rat, -Yder dorp een Stadt, -Yder Stadt een Rijck, -'t Rijck de wereldt heel gelijck. -8. Den Spanjaert door dees heerlijckheydt, -Worde soo verveert, -Dat hy voor onse voeten leydt, -Moordt-priem, spiets, en sweert: -En terstondt // men vernam, -Dat een hondt // word een Lam, -Daer staet // Hollandt in sijn cieraet, -En blinckt dat de son - -'t Nau verdragen kon, -Ia sagh hem schier blindt. -Siet dus heeft ons Godt bemint. -
-
-
- -Hollandts suure Druyven. -
-Stemme: Soo langh is't Muysjen vry. -MAer doe ons Vaderlandt, -Door Godes milde handt -Dus rijck gezegent was, soo terghd'se hem tot wraeck, -En koren niet sijn dienst, maer Bacchus tot vermaeck. -Sy seyden noyt te saem, -Komt danckt des Heeren naem - -Die ons dit alles geeft: en of men rijkdom krijgt -Elc roemt sijn neersticheyt, terwijl hy God verswigt -3. En t'wijl elck een bedenckt, -Niet dat hem Godt dit schenkt, -Maer sijn selfs vernuft, en kloekheyt hem dit deed, -Hy 't niet tot Godes eer, maer eygen lust besteed -4. Hier door soo wordt het Landt -Vervult aen alle kant, -Met wulpsheyt, en met pracht: en na de rijckdom wast, -Wordt oock het Vaderlandt met sonden overlast. -5. En of sijn rijckdom staegh -Vermeerdert alle daegh, -d'Eergiergheydt, en nijt, 't hooveerdige gemoet -Die steeken t hooft noch op ver boven al sijn goet -6. De groote gane voor, -De kleyne volgen 't spoor, - -Dus gaense met malkaer, den wegh al na de Hel, -En die ten Hemel klimt, vint nau een met gesel, -7. Het gantsche Landt dat blinckt: -O neen! het Landt dat stinckt -Van vuyle hoovaerdy, van hoog-moet en van trots, -En smeert sijn vleken aen de beste kind'ren Godts -8. Die nu met sijn verstant -Door-loopt ons Vaderlandt, -'t Is of men nu ter tijdt, geen kind'ren Godts en siet, -Men siet de witte deught, de vreese Godes niet. -9. De Goddeloose klap, -De dulle dronckenschap, -Het geyle Hoer-gesangh dat sweefter over straet: -'t Is of men in het Hof, van 't snoode Romen staet. -10. En t'wijl het dus toegaet, -Soo klinckt op onse straet, - -De stemme van Gots woort, gepredikt in de Kerk -En roept: verbystert volck, verlaet u sondigh werck. -11. Dit is te onbeleeft, -Dat Godt sijn zegen geeft, -Dat hy u Landt met goet, en ghy met quaet vervult: -Voorseecker sijn toorn ontsteeckt om dese schult -12. Dus bromt het heyligh woordt, -En menigh die het hoort, -Die toont dat al de kracht, hier van, in hem verdwijnt: -En 't hoopken is seer kleyn dat nu als lichten schijnt -13. Ia self des Heeren dagh -Moet dienen tot gelagh, -En ongebondentheyt! Ia 't schijnt nu voor gewis, -Dat die des satans dagh, en niet des Heeren is. -14. Een gruwelijcke schant, -Voor u, O dertel Landt! - -Dat ghy door Hemels gunst, met zegen overstort -Vloeyt over in het Goud, en comt in deugt te kort. -
-
-
- -Des Heeren plage over 't Landt. -
-Stemme: Treurt edel huys Nassou. -DOe nu ons Vaderlandt. -Dus tegen Godt aen kant, -En terghd' hem alle dagen,Ontstack oock God de Heer, -En sondt seer vele plagen, -Op dese lande neer. -2. De eerste trof de Staet, - Van Hollandts hooghste Raedt,     Anno 1650 - -Soo dat het Hof der Grooten -Verwerde in 't geheel, -En worde overgoten -Met twist, en met krackeel -3. Wt dese twist, soo quam -Een droeven Oorloghs-vlam, -Die tot den Hemel brande: - Ons Leger quam gegaen,    Den 29. - En rast in dese Lande,         Iulij 1650. -Ons eygen Koop-stadt aen. -6. Hier na noch meerder quam, -Want, den Oranjen stam, -Wt 't edel Hof Nassouwen, -Die al het Landt verheught, - Die worde om-gehouwen,        Den 6. - In 't beste van sijn jeught.        November - - 5. De woeste Zee, oock wordt     Anno 1650 -Door stercken storm geprot, - Die met sijn grijse golven,       Den 5. - Al schuymende aenkoomt,       Maert -En doet den dijck om colven,   1651. -Soo dat hy hene stroomt. -6. Godt sondt sijn plagen mee, -Tot onder 't domme Vee, -Dat schier vergingh in quellingh,   De Na somer - En meest van honger storf,            1651 -Door dien een vreemde swellingh, -Daer mondt, en voet door kort. -7. Den Hemel wort vergramt, -Soo dat hy brandigh vlamt, -En door de Sonne stralen -Versenghd' het Gras, en kruydt:     De Voor En - - En liet geen regen dalen      somer - Ter Hemel-sluysen uyt.       1652. -8. Oock vanght een Oorlogh an - Met Ons, en d'Engels-man,     Gepubliceert -Die met seer vele schepen,     den 2. - Gaet swemmen over Zee:        Augusti. -En geeft ons harde nepen, -En neemt veel schepen mee. -9. Hier door soo worde strack -De Neeringh kranck en swack, -Ia storf ten langen lesten, -En blies het leven uyt: -Ons goedt, sleept in sijn nesten -Den Engels man, tot buyt. -10. De doodt oock, Gods dienstknecht, -Die sijn bevel uyt-recht, - - Schoot sijn vergifte pijlen      Het geheele - Tot in des menschen hert,     Iaer 1652. -Soo dat in korte wijlen -Veel volck begraven werdt. -11. En eer dit Iaer was uyt, -Quam 's avonds in het zuydt,   In December - Een komeet te vooren.              1652 -Als of hy woud' bedien, -Dat Godt sijn strenge tooren, -Noch meer woud' laten sien. -12. En 't is oock soo geschiet, -Een Iaer vol van verdriet,     Anno 1653. -Qaem dese sterre volgen, -En heeft door tegen spoet -Verslonden en verswolgen, -Seer veel van Hollandts goedt: - -13. Ick 't alles niet verhael, -Maer onsen Admirael. - Een van de beste Grooten,           Den 10. - Verlooren wy doe mee,                Augusti -Van d' Engels-man geschooten,  Anno 1658. -By Egmont, in de zee. -14. Oock word in 't Landt gestiert - Een schadelijck gediert:          De gantsche - Veel duysenden van muysen,  somer - Ia een ontelb're som,               1653. -Die al het Gras af pluysen, -En wroeten 't Landt voort om. -15. Den Hemel sagh dit aen,     In septemb. - En schoot een vloedt van traen        1653. -Al weendend' uyt sijn oogen, -En maeckt het Landt, een zee: - -Soo dat de Boeren toogen -Na huys toe, met haer Vee. -16. En oock een storm ontstack, - Die menigh huys verbrack:       Den 7. Ianuarij -Veel schepen zijn bedorven,     1654 -Die op de woeste Zee, -Haer spitse masten korven, -En noch vergingen mee. -17. En in dees storm by nacht, -Daer niemandt op en dacht,     Item. -Soo quam de Brandt verrasschen -De Rijp, dat schoone dorp, -Soo dat het in der asschen, -In korten neder worp. -18. Soo dat ons dagh op dagh, -Nu treffe slagh op slagh: - -O! ghy vervloeckte sonden, -Door u is dit geschiedt, -Dat men hier soo veel wonden, -Door dese slagen, siet. -
-
-
- -Genees-middel. -
-Stem: O jonge jeught bly-hertigh. -MEn hoort nu Hollandt klagen, -En suchten om dees plagen, -Om oorlogh, en om dieren tijdt: -Maer om hier van te zijn bevrijdt, -Soo moetwe in dees dagen, -Ons sonden eerst verjagen, - -En maecken ons dien vyandt quijt: -2. Godts toorn is niet ontsteecken -Op menschen, maer gebreecken: -En buyten sonden is de mensch -Des Heeren lust, en herten wensch: -Wel laet ons die dan kelen, -Soo sal ons Godt weer heelen, -En toonen ons sijn gunst allensch. -3. Als Seba vlucht in Abel, -Quaem David met sijn sabel, -En steld hem soo geweldigh aen, -Als of hy Abel woud' verslaen: -Maer doe na sijn behagen, -Die seba was verslagen, -Is hy van Abel afgegaen. -4. Soo oock, O Hollandts Borgers! - -Gaet, stelt u tot verworgers, -En keelt op sonden, dagh aen dagh, -Elck een soo vele als hy magh: -En Godt sal dit u slachten, -Veel aengenamer achten, -Als Offerhand, van beesten slagh. -5. Oock moeten wy in't midden -Der plagen, Godt aenbidden, -Dat hy ons niet na weerde loon, -Maer door sijn gunst genade toon: -En dat hy in dees dagen, -Ons vry maeckt van dees plagen, -En ons met sijnen zegen kroon. -6. En als hy na 't verschoonen -Der plagen ons doet kroonen -Met sijnen rijcke zegeningh, - -En schenckt ons weder alle dingh, -Soo moetwe Godt van boven, -Voor sijn genade loven, -Daer van ons landt sijn schat ontfingh. -
-
-
- -De kleyne Werelt heeft een Iaer: De Groote, duysent na malkaer. De Lente, -
-Stem: O Flora ydel is u roem. -ALs ick die son verhoogen sie, -En sie hem op, na boven klimmen, - -Wt de Zuyer kimmen, -En door kracht van die, -Het watter, de Lucht, en het Veldt, -In eenen nieuwen Ieught gestelt, -En wassen daeg'lijcks an, -Soo denck ick daer op van: -2. De groote wereldt die gaet voor -De kleyne wereldt volght sijn schreden, -En gaet hem na treden, -In het selve spoor -En dit alleen is het verscheel, -De kleyn gaet, eens, de groot gaet veel. -De kleyne in 't begin, -Die brenght de Lenten in. -3. Dan loopt de Son seer snel om hoogh, -En doet het gantsche Velt in vreughde, - -En een soete jeughde -Vertoonen voor 't oogh: -'t Is even soo, gelijck het soet, -Dat in de Kindtsheydt hem op doet, -Wanneerse in haer Ieught, -Ons toonen groene vreught. -4. Wat teyckens men van vreucht oyt sagh, -Dat van het Veldt, of van de Boomen, -Wy sullen bekomen, -Komen voor den dagh: -Soo gaet het mede met de Ieught, -Men siet 't beginsel van de deught, -Of oock, met siet het zaedt, -Van het volgende quaedt. -5. Ick sie de Son noch hooger gaen, -En schild'ren 't Velt met vele Bloemen, - -Sy zijn niet te noemen, -Die men nu siet staen: -Oock menigh Bloemtjen roodt, en wit, -Op onse Ieught haer wangen sit -Of glinst'ren doormalkaer, -In 't blinckend Goudt-geel haer. -6. Ick hoore nu oock dagen langh, -Een soet, en aengenaem gewemel, -Alsoo dat den Hemel -Klinckt door een gesangh, -Van duysent stemmen te gelijck, -Al sonder voys, of wildt musijck -Alleen gedicht uyt vreught, -Gesongen uyt geneught. -7. Siet hier een aerdigh tegen-beeldt, -Aen het gesaagh, en aen kelen, - -En het lieflijck spelen, -Dat de jonckheydt speelt: -Soo aengenaem, dat al het soet, -Van Vogels daer voor wijcken moet, -Als sy het aerdtsche dal, -Besuyck'ren met geschal. -8. Maer t'wijl de dertjes allegaer, -Nu verkiesen een gesellinne, -De mensch oock uyt minne, -Kiest een weder-paer, -Op dat hy ins dees soete mey, -In soete lusten, met sijn bey, -De wereldt meer verciert, -Met jongh, en soet gediert. -
-
-
- - -De Somer. -
-Stemme: L. Orangie. -ALs hem de son begeeft, -Te wand'len door de kreeft, -Soo dooft te met het geylste groen: -En soo verandert het saysoen -Der tijden // en lijden -Niet meer het soet, -Dat ons de lente broet: -Elck kruydtjen // en spruyten -Sijn groenste cieraet dan verminderen doet. -2. Aldus oock openbaert -Den mannelijcken aert, - -Als dese komt dan gaet sijn gangh -De lossen vreught, en soeten sangh, -Het wesen // voor desen -Haer aengenaem, -Wordt haer dan onbequaem: -De grillen // die stillen -Allenghsjes, en dooven weder te saem. -3. Al 't gewas, en al 't goedt, -Dat heer de wereldt voedt, -Of haer geneest, of oock verblijdt, -'t Wast alles in des somers tijdt, -Dan toone // de schoone -Landen haer vrucht. -De soete somer lucht, -Die voedt haer // en doet haer -Vruchtbaer groeyen tot een yders genucht. - -4. Soo oock, een yder saeck, -Tot nut, en tot vermaeck, -Wordt van de mensche uyt gewracht, -Als hy is in sijn levens kracht: -De Reden // De Leden, -Siel, en Lichaem, -Zijn dan sterck, en bequaem, -En maecken // de saecken -Door de geheele wereldt aengenaem. -5. Maer noyt de son stil staet, -In d' alderhooghste graet: -Maer soo hy quam na boven treen, -Soo stapt hy weder na beneen: -Dan doene // de groene -Velde, den glans -Verdooven, van haer krans: - -Men vindter // en winter, -Nu noodige vruchten al rijp, en gans. -6. Soo oock het stercke bloedt, -Dat is maer als een vloedt, -En neemt haest wederom sijn keer, -En dooft de mensch allenghjes weer: -Dan winnen // de sinnen -Niet meerder aen, -Maer haesten af te gaen: -Maer 't gene // voorhene, -Is geleert, wort dan met vruchte gedaen. -7. Die 't maeyen in den Oost, -Geheel verwareloost, -En niet met allen in en haelt, -Wordt veeltijdts met dit loon betaelt: -Als tijden, van lijden, - -Hem komen aen, -En hem met honger slaen, -Dat klagend // al vragend -Hy seggen sal: Och wat heb ick gedaen? -8. En die hem niet begeeft, -Als hy in krachten leeft, -Om dan te wercken in den Oost, -Daer Godt uyt deelt sijn soeten troost, -En slagen // en plagen, -Hem treffen aen, -Soo sal hy onder 't slaen, -Dan kermen: Och ermen -Wat heb ick al voor een dwaerheydt begaen -
-
-
- - -De Herfst. -
-Stem: Treurt edel huys Nassouw. -ALs ick den Herfst aenschou, -Die met een nare rou, -De Velden, en de Landen, -En al de Boomen stroopt, -Soo dat hy alderhande -Verciersel heel af sloopt: -2. Soo denck ick daetlijck om -Den hoogen ouderdom, -Die nu geheel, en allen, -Des levens groen, en loof, -Verliest, en laet ontvallen, - -En wordt soo dor, en doof. -3. Wanneer den Herfst het Veldt -Becomt in sijn gewelt, -Het set hem strack na treuren: -En 't somers groen vertreckt, -Terwijl met grijse kleuren, -Het landt hem overdeckt. -4. Den mannelijcken glants, -Vergaet oock heel, en gants, -Ontrent de oude jaren: -De schoonheydt die verdooft, -Terwijl de grijse haren. -Aenkippen op het hooft. -5. En alderhande kruydt, -Dat in de Lente spruyt, -Of somers is gewassen, - -Dat heeft den Herfst dan in -Syn Schuuren, en sijn Kassen -Tot nut van 't Huys-gesin. -6. Soo oock, wat voor deught, -Geleert is in de Ieught, -Of in de manlijckheden, -Dat is dan in 't gemeen, -Van hem die leefd' na reden -In d' ouderdom by een. -7. Wanneer de Son gaet leegh, -Dan treffen ons ter deegh, -De pijlen van de koude: -En dat wy door dit kruys -D[u]s aengeprickelt, soude -Verlangen na ons huys -8. Als dan een mensch gevoelt, - -Dat hem het bloedt' verkoelt, -De krachten hem begeven: -Soo noem hem vry geen mensch, -Soo hy na 't Hemels leven, -Niet hoopt, met al sijn wensch. -
-
-
- -De Winter. -
-Stem: Amarilli mia bella. -WAnneer de Son sijn stralen -Van ons ontreckt, en op den moor doet dalen, -Wordt het in onse palen -Geheel ontciert: Daer blijft noch kruyt, noch lover -In al de Velden over - -Dan valt de natuur, gelijck als in 't beswijmen: -En de Landen // als de zanden // aen de stranden, -Wit berijmen. -2. Wanneer ick hier op achte, -Soo valt my in, terstont in mijn gedachte, -Dus is 't met 's Menschen krachte: -Als Godt de Lucht, die wy ter Neus in snuyven, -Op sijn woordt, doet verstuyven, -Dat is al 't geen, dat dus lang noch heeft geschenen -Voor sijn sonne // nu verwonnen // nu verslonnen, -Nu verdwenen. -3. De Boomen op de Velde, -Die door 't gewicht der vreuchten neder helde -En wat ons vrucht bestelde, -Staen nu gestroopt, en hebben 't al verlooren: -De wat'ren zijn bevroren, - -Dat onlangs vloeyd, is nu harder als een Nagel. -'t Velt gaet schuylen, als in kuylen // door der buyien -Sneeuw, en Hagel. -4. Soo oock, de mensch na 't leven, -En sal niet meer aen yemandt teecken geven, -Van 't geen hy heeft bedreven, -'t Is alles uyt: het bloet dat door sijn leden, -Heeft op en neer gereden, -Dat stremt tot ys: dus comt de vorst in hem selven, -En dan spoetmen // want dan doetmen // ja dan moetmen -Hem bedelven. -5. Daer rust hy in sijn Kamer: -Geen Koninghs-hof is tot de rust bequaemer: -Als de straf met sijn Hamer, -Van Godt gestuurt, komt over 't Landt te sweven -Om menigh slagh te geven. - -Soo rust hy daer, om de straffe te ontschuylen, -Vry van plage // vry van slage // vry van klage -Vry van huylen. -6. Oock sal hy daer niet wesen -Voor alle tijdt: maer als komt opgeresen, -Die Soone, die door desen -Ver boven Son en sterren is geweken, -En van daer sal voort breken, -Met soo een glans, die dees Son sal doen vertrecken, -Dan sal desen // opgeresen // wesens wesen -Hem op-wecken. -7. En brengen in de woonningh, -Daer 't eeuwigh blincken aen hem geeft vertooning -d' Heerlijckheydt van dien Koningh -Die sterck bemuurt, met der Engelen scharen, -Dus eeuwigh, sonder jaren - -Leeft sonder end. Geeft o Heer ons al te samen, -Dat na 't sterven // wy verwerven // 's Hemels erven, -By u Amen. -
-
-
- -Een Weeck toont ons in volle daet, Hoe dat het met ons leven gaet -
-Stem: Wel dus bedroeft Ionckvrou? -WIl yemandt in het kort -Eens sien des menschen leven, -Die kan 't haest sien: want 't wordt -Ons in een Weeck beschreven, -De vreught, en doefheydt door malkaer, - -Ia 't leven hier, en oock hier naer, -Blijckt alle weecken klaer. -2. Een gantsche weeck bestaet -Wt daghen, ende nachten, -Wanneer den eenen gaet, -Treedt d' ander op sijn wachte: -En t'wijl den dagh volvoert haer plicht, -Soo toont sy ons een helder licht: -De nacht een naer gesicht. -3. Dus is ons leven mee -Te saem in een geweven, -Van onrust, en van vree, -Van droef, en vrolijck leven. -'t Is altemet een ander stuck: -Somtijds beschijnt ons het geluck, -Somtijdts ontmoet ons druckt. - -4. Een weeck in haer aenvangh -Brenght ons den arbeydt mede, -En geeft in haer voortgangh -Ons seer vermoeyde leden: -Den arbeydt, met haer ommeslagh, -Doet, datmen na den Saterdagh -Hier wel verlangen magh. -5. Dus vanght oock 't leven aen -Met veel onrustigh woelen, -En doet ons in 't voortgaen -Veel ongemack gevoelen: -Soo dat men met verlangen groot, -Magh hopen Chris'lijck na de doodt, -Die ons treckt uyt dees noot. -6. Maer na een droeve ry -Van dagen vol van woelen, - -Komt ons den Son-dagh by, -En doet ons rust gevoelen -Dan werckt de ziele hare plicht, -En wordt in Godes huys gesticht, -Door 't soete Hemels licht. -7. Dit beeldt ons 't leven af -Dat Godt de ziel sal geven, -(Als 't lichaem rust in 't graf. -By hem, in 't eeuwigh leven -Godts lof is daer haer daegh'lijcks liedt, -Terwijl dat elck een vreught geniet, -Als hier noyt oogh en siet. -
Den dagh beelt ons het leven af, -De nacht de doot, en 't duyster graf -
-
-
- - -Den Dagh. -
-Stemme: Edel Karsouw. -WAnneer oprijst // de Sonne in den morgen, -En op gaet in sijn kracht, -Hy ons aenwijst // het leven vol van sorgen, -Van 't menschelijck geslacht: -En soo men betracht, -Het menschelijcke leven, -Siet men 't klaer // en by malkaer, -Op eenen dagh beschreven. -2. De morgen-stont // die met sijn gouden vlamme -Het gantsche Landt vergult, -toont ons de mont // des kints, dat an de mamme - -V hert met vreught vervult, -Als ghy aensien sult -Sijn soete bolle kaecken: -Sijn gesicht // en 't morgen-licht, -Zijn twee gelijcke saecken. -3. Het hooger gaen // der Son, in de' eerste uren, -Tot hem de middagh keert, -Dat wijst ons aen // den aenwas der nature -Die dus haer kracht vermeert. -Sooje meer begeeert, -Ghy sult Bruyloft na 't trouwen, -In 't onthael // van 't middagh-mael, -Gneughelijck aenschouwen. -4. Maer desen stant // en blijft niet in een wesen, -De sonne daelt haest neer, -En sijnen brandt // en heete vlam na desen, - -Die dooft allenghsjes weer. -Oock de mensch, goe seer, -Hy is verciert in allen, -Sal nochtans // sijn schoone glans -En kracht, te met vervallen. -5. Maer als de son // aleer hy komt te scheyden, -Gaet na beneden toe, -Wie oyt begon // te wercken, en arbeyden, -Die wordt dan mat, en moe: -Elck gaet na huys toe, -En laet sijn arbeydts saecken, -Om 't gemack // dan onder 't dack, -In d' Avondt-stondt te smaecken. -6. En even dat // sietmen oock soo geschieden, -Op 't hooghst van 's levens trap, -Dat moed en mat // in d'ouderdom, de lieden, - -Haer krachten worden slap -Maer de wetenschap -Van onmacht, en swackheden, -Wijst haer aen // om dan voortaen, -Te rusten hare leden. -
-
-
- -De Nacht. -
-Stem: Carileen ay wilt u niet verschuylen. -Als in koelt // de nacht komt overkleeden -Met veldt, met een swarten schijn, -Soo daelt dan op ons weer, -Van boven neer -De soete slaep // daer door wy in ruste zijn. - -Dan en voelt // men in de gantsche leden, -Geen vermoeytheydt, noch verdriet, -Maer al het ongemack, -Dat 's daeghs ons stack, -Dat is door de soete slaep geheel te niet. -En op dit sachte -Op-geschudde dons, -Troulijck over ons. -En laet // het quaet // van d' Helsche macht -Niet toe // dat het doe // het gene daer 't na tracht. -2. 'k Hier in vind // na 't leven afgebeeldet, -Den doodt, en het geen sy doet. -Sy komt in swarten schijn, -Van smert en pijn, -Maer onder dat swert schuylt oock een ruste soet - -Die Godts kindt // altijdt sijn smerten heeldet, -En maeckt dat Godts volck vergeet -Des levens ongeluck, -Verdriet, en druck, -Droefheyt, en ongeval: Kortlijck, al haer leet. -Die Israel hoedet, -Self oock in der nacht, -Wanneer als woedet -Des Satans geslacht, -'t Welck meent // 't gebeente te rooven dan, -Betemt, hy, en demt // het, dat het niet en kan -3. Als dus is // de mensch in 't graf gelegen, -(Even of hy in der nacht, -Op 't bedde sacht gespryt, -Was neer geleydt, -Daer hy in stille rust na de morgen wacht, - -Om seer fis // verquickt, hem te bewegen: -De verwacht oock eenen dagh, -Daer in hy los geret -Wt des doodts net, -In een beter leven hem begeven magh -Een sulcken leven, -Dat een nieuwe jeught, -Aen hem sal geven, -Vol van soete vreught, -Daer oyt // noch noyt // den ouden dagh, -Het soet // van het goedt, verderven kan, noch magh -
-
-
- -Hemelsche Vreughde. -
-Stem: Als Boecxvoetjen speelt. - -DE zielen die vry van het lijdende vleys, -Ontbonden, van sonden, gaen trecken op reys -En d' Engelen schoon // uyt d' Hemelsche troon, -Dan dragen by Godt in sijn hoogh Paleys. -2. Die genieten aldaer het salighe goedt, -En schincken, en drincken, het Hemelsche soet. -En singen Godts lof // in 't Hemelsche Hof, -Vol salige vreughden in haer gemoedt. -3. Daer sietmen geen Sonne, en nochtans geen nacht -Daer vliete, noch schiete, geen tranen noch klacht -Daer licht haer geen vlam // haer Keersse is 't lam -Dat voor haer benden hier is geslacht. -4. Daer vreestmen geen vyandt, die ruste verstoort -Van buyten, noch sluyten van binnen geen poort -Geen lijden, noch pijn // en salder in zijn, - -Soo datmen geen kermen, noch klagen hoort. -5. De zielen oock self in den Hemel geleydt, -Die blincken, door 't schincken van d' Heerelijckheyt: -Soo datse daer zijn // in heerlijcker schijn, -Als 't schijnsel der Sonne, alhier verspreyt. -6. Soo dat al het blinken van koningen pracht -Die troonen, of kroonen, op aerden aenbracht, -Wanneer men 't gelijckt // met 't gene daer blijckt -'t Is niet met allen, by 't selve geacht. -7. Als nu maer een droopje de Heere ons doet -Hier smaken, wy raken terstondt in 't gemoedt -Vol vreughde daer van; wat sal het zijn, dan -Als wy sullen smaken de gantsche vloet? -8. 't Is seeker dat niemant alhier op der aerdt -Kan vatten, de schatten by Gode bewaert: -Hoedanigh, hoeveel // sal wesen dat deel, - -Wordt noyt ter degen beneden verklaert. -9. Dit leven dus heerlijk, daer in men den Heer -Daer boven, sal loven, en singen hem eer -Dat streckt hem seer wijt // ja buyten de tijdt, -En daerom sal 't eyndigen nimmermeer. -
-
-
- - - - - -Op de Vrede Gemaeckt tusschen de E. Heeren Staten deser Landen, ende den Protector van Engelandt. Gepubliceert den 18. Mey 1654. -
-Stem: Wilhelmus van Nassouwe. - -HEf op u hert en handen, -En danckt den goeden Godt, -O vrye Nederlanden, -Voor u geluckigh lot: -Godt heeft ons in ons dagen -Met zegen overstort, -En wederom sijn plagen, -En slagen, opgeschort. -2. Wy hebben langh tevoren, -Door sonden, boos en quaedt, -Ontsteken Godes tooren: -Maer hy rijck in genaedt, -Die doet ons eerst waerschouwen -Eer hy ten vollen slaet, -Op dat ons soo mocht rouwen -Ons krijtende misdaedt. - -3. Godt sondt na onse Naesten -Een wer-geest, met onvreed, -Die aldaer in der haesten -Het sweert trock uyt de scheed, -Daer d'Engels-man mee snede -Het oudt verbondt in twee: -Soo dat hy al nam mede -Wat Hollandt stierd' op Zee. -4. Wat tonge kan nu melden -'t Geen als doe is geschiedt? -Want Tromp' het hooft der helden, -Daer door sijn leven liet. -De Neeringh-rijcke Steden, -Die worden neeringhloos: -De Burgery onvreden, -Door schaed op schade altoos. - -5. Dit was Godts volck een jammer, -Een droefheydt, en hert seer: -En hier uyt oock soo quammer -Veel suchtens tot den Heer: -De stemme veler volcken -Quam daeghlijcks voor Godts troon, -Daer drongh tot door de wolcken -Een roepen, Heer verschoon. -6. En Godt, die in sijn handen -Het hert der Vorsten draeght, -En daer door dese Landen -Twee Iaren heeft geplaeght, -Die doet nu weder staken -Den trots van ons Vyandt, -Alsoo dat hy gaet maken -Een Vrede met Hollandt. - -7. 't Is wel een gulde strale -Daer door de nacht verdwijnt -Die uyt des Hemels sale -Op des Landen schijnt: -Nu gaen de Nederlanden -Weer swanger van vermaeck, -Terwijl men sluyt in banden -De moed-wil, en de wraeck -8. Dus doet ons Godt oprijsen, -Gelijck als uyt het Graf -Wie soude hem niet prijsen, -Die ons dit alles gaf? -Het is de handt des Heeren -Die alleen wonder doet, -Die 't alles kan verkeeren, -En schept het quaedt, en 't goedt. - -9. Maer lieve Heer der Heeren, -Bescherm-Heer van ons Staet, -Wilt voortaen t' uwer eere -Ons kroonen met genaedt: -Siet niet aen ons gebreecken, -Waer door wy menigh-werf -V tergen, om te wreecken -Ons quaedt, tot ons verderf. -10. Breeck af de snoode lagen -Tot hinder van ons Staet: -Och Heere wilt verjagen -Al 't geen niet recht en gaet: -Ghy hebt ons vreed' gegeven, -Geef daer u zegen mee: -En geef ons daer beneden -Voor al met u goe vree. -
-
-
- - -Vermaninge aen de Nederlanden, by occasie van de teghenwoordighe Vrede. -
-Stem: Geklaeght zy u o Heer der Heeren -WAeck op, verstockte Nederlanden, -En volgh soo niet u boose lust, -Eer Godes toorn begint te branden. -En set in vlam u sachte rust: -Godt stelt ons vele teeckens vooren, -Daer door dat hy -Geduurigh roept in onse ooren: -Keert u tot my. -2. Onlangs dreyghd' hy ons door veel plagen -En toond hem gram in alle ding - -Maer heden nu, in onse dagen, -Weckt hy ons op door zegeningh: -Hy heeft sijn milde handt ontsloten, -En niet gebeydt, -Maer haestigh op ons neer-gegoten -Sijn goedigheydt -3. Dit doet hy om ons op te wecken, -En af te leyden van het quaed, -En door sijn goedt ons te trecken -Met sachte koorden van genaed. -Maer kan dit ons nu niet bewegen -Tot beterschap, -Soo sal hy hem in toorne tegen -Ons stellen t' schrap. -4. Om tot den gront ons te verderven, -Gelijck een vyer dat vreeslijck vlamt - -Waer sal dan 't schepsel troost verwerven, -Als dus den schepper is vergramt? -Voorwaer in Hemel, noch op Aerden, -En isser dan -Niet een soo sterck, noch groot in waerden -Die duuren kan -5. Ia alle scheps'len die wy vinden, -Dat zijn al dienaers onder hem, -Den Hemel, Aerd, en Zee, en winden, -Staen strack bereydt op sijnen stem. -Sijn pijlen heeft hy noyt verschoten, -En nimmermeer -Zijn sijn Fiolen leegh gegoten, -Van boven neer. -6. 't Is dwaesheyt van ons op te maken, -En teghen Godt te kanten aen: - -Want als sijn toorne is aen 't blaken, -Soo kan geen mensch voor hem bestaen: -Wel laet ons dan voor sijne slagen -Seer zijn vertsaeght, -En onse sonden van ons iagen, -Om 't welck hy plaeght. -7. Wy hebben ons met Godt verbonden -Tot een parthy van sijn Vyandt, -En hy voert oorlogh met de sonden: -Wat doen die dan in't Vaderlandt? -Soo wy dien Vyandt niet verdrijven -Wt onse Landt, -Hoe kan met Godt ons Vni blijven -In sijnen stant? -8. Wel laet u dan, o Landt waerschouwen, -'t Wijl't noch tijdt is om af te staen, - -Eer Godt om dese quaede trouwe -Ons als Rebellen komt te slaen. -Want hy, wiens oogen zijn als Sonnen -Op yder plaets, -Die sal niet van ons lijden konnen -Soo vele quaedts. -
-
-
- - - - -Veldt-Sangh, Op 't vreuchtbare ghewas in Iulius 1654 -
-Stemme: Nere, schoonste van uw' gebuuren. - -DOe ick onlanghs de ruyge Velden -Met mijnen gladden Zeyn beschoer, -Gingh dus mijn keel Godts goedtheydt melden, -In 't zegenen van yder Boer: -2. Als Godt den Hemel doet verhooren, -En d' Hemel weer het Aerdtsche-dal, -En d' Aerde dan de Wijn, en 't Kooren, -En 't Koorn de menschen over al, -3. Soo komt in haest des Heeren zegen, -Met rijcke schatten, veelderley, -En laeft ons menschen, als de regen -Het groene Kruydt doet in de mey. -4. Met reden mogen wy dus seggen, -In 't schoone Dorp van Wervershoof, -Daer men onlanghs het Veldt sagh leggen, -Seer mager, schrael, en dor, en doof. - -5. In 't alderschoonste van de meye, -Doe was het landt noch wit, en swert, -Men sagh nau groene tusschen beyen, -Nau Beest op 't landt gevonden werdt. -6. Een yders hert was heel besloten, -En toe-geschroeft door 't swaer verdriet, -En 't Landt met plagen overgoten, -In 't welck men Godes toorne siet. -7. Wy sagen klaerlijck voor ons oogen -Een grooten druck, en swaren noot, -Ten zy dat Godt uyt mede doogen, -Ons weer sijn milden zegen boodt. -8. Godt doet oock haest sijn heyl weer toonen, -Midts hy ons op het spoedighst helpt, -En met sijn zegen ons doet kroonen, -En met veel gaven overstelpt. - -9. Het Landt, met jeughdigh gras besproten, -Was onlanghs slijck, maer staet nu groen, -Midts duysent duysent jonge loten, -Haer in der haest vertoonen doen: -10. Die tegen al 't vernuft, en reden, -Opschieten haest, men weet niet hoe: -Soo dat de gladde koeyen treden -In 't Gras, by na aen d' ooren toe. -11. Het hoy, dat men schier most opwegen -Met al ons geldt, wanneer men 't kocht -Heeft ons de Heer door sijnen zegen, -In overvloedt nu toe-gebrocht. -12. Hy heeft ons Koorn, en Terruw geschonken -Die d' Acker siet, het is genucht: -En onse Boomen staen en proncken, -Met alderhande leck're vrucht. - -13. Dus toont de Heer te met zijn krachten -In wonder wercken, ongemeyn: -Doe Simson woud' van dorst versmachten, -Maeckt Godt een kin-back een Fonteyn. -14. Elias, in gebreck van spijse, -Kreegh door een Raven sijnen kost, -En op een wonderbare wijse -Heeft Godt Samaria verlost. -15. Dus werckt de groote Godt van boven, -Op dat wy in voorspoedigheydt, -Niet ons vernuft en souden loven, -Maer alleen sijne goedigheydt. -16. Wy dancken u dan, Heer der Heeren, -Voor al 't geen dat ons landt ontfingh: -En wilt ons voort, tot uwer eeren, -By blijven met uw zegeningh. -
-
-
- - -Passien, en Reden. -
-Stem: Cloris aen de Water-stroomen, -DIt is licht voor elck te lesen: -Sonder Passien te wesen, -Dat is min zijn als een Beest: -Maer soo wie hem heeft begeven, -Om na Reden niet te leven, -Die en heeft geen menschen geest. -2 Daer was noyt yemandt op Eerde, -Diese alle bey ontbeerde, -Maer wel menigh mister een: -Daerom is het wel een zegen, -Diese beyde heeft verkregen, - -En de Passy stuurt door Reen. -3 't Strijdt de Reden niet en tegen, -Dat de Passijen haer bewegen, -Maer dat sy gants qualijck gaen: -Daerom moet de siende Reden -Voor de blinde Passy treden, -Leyden die te rechte aen. -4 Ick wil droef, en vrolijck wesen, -Wanneer als tot beyde desen -My de siende reden wijst; -Maer ick wil my noyt verblijden, -Noch niet suchten in mijn lijden, -Dat het boven Reden rijst. -5 Ick wil soo mijn vreughde stieren, -Dat daer door in geen manieren -My een ongeluck aentreft, - -Ick wil alsoo matigh treuren, -Dat daer door noyt sal gebeuren, -Dat mijn leet daer door verheft. -6 Dat in ons de Passien leven, -Kan aen niemandt teecken geven -Van een dwaes, of slecht verstant: -Maer die ons te kennen geven, -Dat sy sonder Reden leven, -Zijn de Sotste die men vandt. -
-
- - - - - - -Bruylofts-geschenck Op 't Huw'lijck van De. Hubertus van der Meer, Predicant tot Wevershove. - -Met Iuffr. Geertruyde Maria Bailly. Getrout tot Amsterdam den 25. Augusti 1654. -
-Stemme: Al wat men hier in dese wereldt siet, -ALs van der meer op Eng'le-vleug'len vloogh, -Sijn heyl'ge Ziel ontlast van teug'len, toogh -Ten Hemel-waert, en sagh in 't hooge koor, -Het Hemels-huys met geest'lijcke oogen door, - -2. Daer yder ziel Godts lieflijckheden siet: -Dus quam sijn geest alhier beneden niet, -Als dan alleen, wanneer door 't schijnent' licht -Van Godes wordt, hy 't volck tot sijnend sticht. -3. Maer t' wijl om hoogh, hy heen en weder vloog, -Hy eens't gesicht na d' Aerde neder boogh, -Daer hem een glants als van veel Peerels t'saem - In d'oogen blonk, beneen uyt's Amsterdam Werelts-kraem -4. Dies hy om laegh gelijck een Sterre schiet, -Na 't lieve licht dat hy van verre siet: -Maer doe hy nu by dese Vlam-ster quam, -Doe was 't Bailly' het puyck van Amsterdam. -5. Een maegt wiens deugt meer als cieraden blonck, -Daer aen Natuur al haer weldaden schonck: - -Wiens lieflijck oogh van minlijckheden buurt, -'t Welck sy alleen na deught en reden stuurt -6. Wiens kuyssche oor noyt stem ontfangen most -Wanneer d'ontucht haer swang're wangen lost. -Wiens tongh bewijst, dat waerlijck dese maeght -Geen aerdts, maer self een Hemels wesen draegt -7. Doe van der meer dit puyck der jeught vernam -En hy 't gesicht van hare deught bequaem -Wiens glants dat self een Hemels schoonheydt was -Is hem door dese ongewoonheydt ras -8. Een vuur ontfonkt, een suyv're minne-vlam -Die door't gesticht van dees vriendinne quam. -En t'wijl sijn ziel tot haer getogen werdt, -Sprack hy haer aen uyt een bewogen hert, -9. Met soo een tael waer uyt sy klaerlijck las -Dat dat de stem der minne waerlijck was. - -Dies sy (van aerdt gelijck als hy gestelt) -Oock word geboeyt en vast, door 't vry gewelt, -10. Van minne dwangh, die stelt door eenen vlam -Die van de deugt des minnaers hene quam -Haer hert in lichten vlam, door minne-brandt: -Daer smelt haer bey het hert by 't ingewandt, -11. Gelijck te met de held're witte snee -Door Sonne-schijn of and're hitte dee, -Den Hemel die sagh dese brandt opgaen, -Dies stack sy daer terstondt de handt op aen, -12. Daer sy die twee te samen mee men'len dee -Ia self ziel te saen dee streug'len mee. -O waerd gespant dat nu in soeter vreught, -Op 't Echte bedt uw' lusten boeten meught, -13. En met malkaer, in minne, plegen 't soet, -Dat al de vreught, en lust, opwegen doet: - -Treedt vrolijck heen, na 't bruylofts Liedekant, -Alwaer de min haer lieflijckheden plant, -14. En doet op-schieten, lusten voor u bey. -Maer stil mijn pen: my dunckt ick hoor een Rey, -Daer yder keel klinckt als een fijne Fluyt, -Op d' Echt van van der meer en sijne bruydt. -
-
- -Stemme: Maskurade, Of: 't Visschen moet wel vreughde baren. -OM de Echt van dees Gepaerde, -t' Saem gesmolten in haer ieught, - -Zijn de Hemel, en de aerde, -Beyde vol van soeten vreught. -2. 't Segen-rijcke wervershove -Wordt met Hemels glants bedeckt, -'t Gaet den Amstel-Staedt te boven, -Daer de Sonne van vertreckt. -3. Wat is 't, of haer blauwe Toorens -Door wolcken henen gaen, -En sy met haer spitse hoorens -Dreyght te stooten aen de maen? -4. Al haer glants sal heel verdwijnen, -Onder 't Rou-kleedt van de nacht, -Daerm' in Wervershoof het schijnen -Van twee held're Sonnen wacht. -5. Driemael Heyligh, eeuwigh wesen, -Stichter van den Echten-staet, - -Laet nu bloeyen over desen, -Eenen stroom van Honigh-raedt. -6. Laet de Bruydt in't Huw'lijck wesen -Eenen Wijn-stock daer, o Heer -Vwe Kercke van magh lesen -Menigh iongen van der meer. -7. Die uw volck noch mogen leyden, -Door uw waerheydts held're schijn, -Als haer ouders, alle beyden -By u al ten Hemel zijn. -8. Geef oock dit, O Godt daer boven, -Tot ons heyl, en tot uw' lof, -Dat sy zijn in wervershove -Tot sy gaen in 't Hemels-Hof. -Anagramma. - -Hubertus van der Meer. -Is Wervershover eer, -En Bailly 's herten-lust: -Dus volght hy het beleydt -Van sijnen naem, welck seydt -Beraem hun Vrede-rust. -
-
-
- - - - - -Wellekomst, Aen Iuffr. Geertruyde Maria Bailly. Gekomen tot Wervershoof den 5. September 1654. -
- -Stemme: Roosemont die lagh gedoken. - WElkom, welkom waerde Vrouwe, -Puyck-cieraet van eer, en deught, -Die den Amstel stelt in rouwe, -Ende wervershoof in vreught: -Elck met ernst soo 't sijne pleght, -Dat het d' Hemel self beweeght. -2. d' Amstel weende uyt haer oogen, -Door 't verlies, van droefheydt seer: -d' Hemel schoot uyt mede-doogen -Oock terstondt haer tranen neer: -'t Scheen haer druppen letten woud, -Dat ghy niet vertrecken soud. -3. Maer doe sy ter ander zijde -Sagh de vreught van Wervershoof. - -Toond' haer d' Hemel weder blijde, -En sy 't swart Gordijn verschoof: -Ende sloot haer druypend' nat -Dicht in hooge water-vat. -4. Van dat 's morgens, Phebi paerden -Quamen voort met 't gulde Hooft, -Tot dat 's avondts, onder d' Aerde, -Thitis-plas dien Fackel dooft, -Saghmen in haer aengesicht. -Niet als lacchend' blinckend' licht. -5. Was ons u, o waerde Vrouwe, -Dat de Son dus vrolijck scheen. -d' Hemel zegen voort u trouwe, -Ende gun dat ghy gemeen -Met uw' lieven weder-paer, -Hier meught leven hondert-jaer. -
-
-
- - -Sluyt-Veers. -Dit Veersje dient noch tot besluyt, -En daer mee is mijn Boeckjen uyt, -Is noch uw' sangh-lust niet voldaen, -Soo singh het weer van voeren aen. -
-
- Register der Liedekens.
@@ -4259,145 +219,6 @@ Met Iuffr. Geertruyde Maria Bailly. Getrout tot Amst Als van der meer op Eng'le-vleug'len vloog.232
-
-D. - - De wijsheydt die is uyt gevaren.11 - De soete mey17 -
- - - Doe de Koningh dit aenhoorde.33 - David die nu had verstaen.36 - Doe de hoogen Godt op eerde.39 - Des Hemels licht verdween44 - Doe eertijdts Iohannes vernam.55 - Daer zijn geen saucen die soeter doen smaken72 - De valsche vrinde.80 - Daer is geen staet te noemen.89 - Draeght nu vry geen swarte rocken116 - Daer quam van eenen eter spijs.147 - Doe nu ons Vaderlandt.174 - De zielen die vry van het lijdende vleys.212 - Doe ick onlanghs de ruyge velden.225 - Dit is licht voor elck te lesen.229 -
-
-
- -E. - - Een goedt verstant, een wijs beleyt.28 - Een mensch die hoord te weten.47 - Een Aexter was eens bloot en kael.57 - Een Aep die eenen vreemden lust.60 - Een oude Fabel, van lange wijl.78 - En roemt niet van u Landt, noch Stadt.96 - Een mensche van natuur.110 - Een moordenaer die altoos // Goddeloos.132 -
-
-
-G. - - Ghy dochters iongh en teer.22 - Geluck, ghy die de tijdt van strijdt.82 -
- - - Gelijck een Vogel die daer henen.123 - Ghy die aen lust, en weeldt.155 - Gelijck Godt met sijn Wijnbergh dee.164 -
-
-
-H. - - Het is een wonder om aen te mercken.67 - Hef op, mijn Sangh-Goddin een Liedt.85 - Het eerste daghjen 't Nieuwe-Iaer.158 - Hef op u hert en handen.212 -
-
-
-I. - - Indienje eens willet letten.103 - Ick voel mijn Geest gedreven.119 - Ick was schier geweken.129 - Ick weet Heere, ghy sijt seecker.142 -
-
-
- -K. - - Keerom, keerom, o Wereldts Kindt68 - 'k Wil de Liefste diemen siet.160 -
-
-
-L. - - Looft Godt alle ghy verloste.139 -
-
-
-M. - - Men siet de Min, en haren dwangh.20 - My dunckt dan eenen veugel.51 - Maer doe ons Vaderlandt.170 - Men hoort nu Hollandt klagen.181 -
-
-
- -O. - - O soete Ieught, die in u jeughde zijt.8 - O groot geluck voor die 't geniet.14 - O soete jeught, weest niet hooveerdigh.150 - Om de Echt van dees gepaerde.237 -
-
-
-V. - - Vraeghje wie het meeste goedt.107 - Voor swellen die besloten zijn.136 -
-
-
-W. - - Wat is de jeught, en 't lieflijck bloosen.25 - Wilje wercken op goe voet.64 -
- - - Wanneer als Godt sijn strengh gericht.75 - Wilje u vermaken.93 - Wilje weten wat de Neeringh.100 - Wie wel bemerckt de ordeningh.113 - Wie ist, die vry, met ongeboeyde ziele.26 - Wanneer de Son sijn stralen.197 - Wil yemandt in het kort.201 - Wanneer oprijst, de Sonne in den morgen.209 - Waeck op verstockte Nederlanden.220 - Welkom, welkom, waerde Vrouwe.240 -
-

 

-

FINIS.

- - -

 

-

Tot Harlingen,

-

 

-

Gedruckt by Evert Idzes van Doorn. Ordinaris Boeck-drucker deser Stede, wonende op de Bree-Plaets 1671.

-

 

-
-
diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 33c7a9e5e..f7d4e1389 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -117,9 +117,20 @@ def dbnl_corpus(settings): 'chapter_title': 'Op De vermakelijke en stightelijke Liedekens van Cornelis Maarts', 'chapter_index': 2, 'is_primary': False, - } -] + [{}] * 68 + [ # skip to the next book - { + }, { + 'chapter_title': 'Register der Liedekens.', + 'content': '\n'.join([ + 'Register der Liedekens.', + 'A.', + 'ACh gesalfde van den Heer. Pag. 30 ', + 'Als Saul, en david den vyant in\'t velt. 41 ', + 'Als ick de Son verhoogen sie. 184 ', + 'Als hem de Son begeeft. 189 ', + 'Als ick den Herfst aenschou. 194 ', + 'Als in koelt, de nacht komt overkleeden 208 ', + 'Als van der meer op Eng\'le-vleug\'len vloog. 232', + ]) + }, { # metadata-only book 'title_id': 'maer002alex01', 'title': 'Alexanders geesten', 'year_full': '13de eeuw', @@ -151,7 +162,7 @@ def test_dbnl_extraction(dbnl_corpus): corpus = load_corpus(dbnl_corpus) docs = list(corpus.documents()) - assert len(docs) == 70 + 6 # 70 chapters + 6 metadata-only books + assert len(docs) == 3 + 6 # 70 chapters + 6 metadata-only books for actual, expected in zip(docs, expected_docs): # assert that actual is a superset of expected From 6caea271d029b5b02075cd477393f4d7aafbe8ce Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 11 May 2023 10:27:19 +0200 Subject: [PATCH 055/262] set role of corpus links --- .../corpus-selector/corpus-selector.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html index 72b51f8eb..2c84b5973 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -6,7 +6,7 @@
-

+

{{corpus.title}}

From a2977c83a2824269a8ae39ff9f5b9ff88da74690 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 17 May 2023 14:27:56 +0200 Subject: [PATCH 056/262] correct year filter range --- backend/corpora/dbnl/dbnl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index a1400148e..ecaa8d7a7 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -133,7 +133,7 @@ def _xml_files(self): es_mapping=int_mapping(), search_filter=RangeFilter( description='Select books by publication year', - lower=1200, upper=2020 + lower=1200, upper=1890 ), visualizations=['resultscount', 'termfrequency'], sortable=True, From 0b7995888f1b6afd48f6b5cf3c77333a5827b7a1 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 17 May 2023 14:34:47 +0200 Subject: [PATCH 057/262] improve module imports --- backend/corpora/dbnl/dbnl.py | 51 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index ecaa8d7a7..a00b6d6b0 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -2,12 +2,11 @@ import os import re from tqdm import tqdm -import random from django.conf import settings from addcorpus.corpus import XMLCorpus, Field from addcorpus.extract import Metadata, XML, Pass, Index, Backup, Combined -from corpora.dbnl.utils import * +import corpora.dbnl.utils as utils from addcorpus.es_mappings import * from addcorpus.filters import RangeFilter, MultipleChoiceFilter, BooleanFilter @@ -35,7 +34,7 @@ class DBNL(XMLCorpus): def sources(self, start = None, end = None): csv_path = os.path.join(self.data_directory, 'titels_pd.csv') - all_metadata = extract_metadata(csv_path) + all_metadata = utils.extract_metadata(csv_path) print('Extracting XML files...') for id, path in tqdm(list(self._xml_files())): @@ -49,14 +48,14 @@ def sources(self, start = None, end = None): year = int(metadata['_jaar']) - if between_years(year, start, end): + if utils.between_years(year, start, end): yield path, metadata # we popped metadata while going through the XMLs # now add data for the remaining records (without text) print('Extracting metadata-only records...') - with BlankXML(self.data_directory) as blank_file: + with utils.BlankXML(self.data_directory) as blank_file: for id in tqdm(all_metadata): csv_metadata = all_metadata[id] metadata = { @@ -65,7 +64,7 @@ def sources(self, start = None, end = None): **csv_metadata } year = int(metadata['_jaar']) - if between_years(year, start, end): + if utils.between_years(year, start, end): yield blank_file, metadata def _xml_files(self): @@ -168,12 +167,12 @@ def _xml_files(self): results_overview=True, search_field_core=True, csv_core=True, - extractor=join_extracted( + extractor=utils.join_extracted( Combined( - author_extractor('voornaam'), - author_extractor('voorvoegsel'), - author_extractor('achternaam'), - transform=lambda values: [format_name(parts) for parts in zip(*values)] + utils.author_extractor('voornaam'), + utils.author_extractor('voorvoegsel'), + utils.author_extractor('achternaam'), + transform=lambda values: [utils.format_name(parts) for parts in zip(*values)] ) ), es_mapping=keyword_mapping(enable_full_text_search=True), @@ -184,7 +183,7 @@ def _xml_files(self): name='author_id', display_name='Author ID', description='ID(s) of the author(s)', - extractor=author_single_value_extractor('pers_id'), + extractor=utils.author_single_value_extractor('pers_id'), es_mapping=keyword_mapping(), ) @@ -192,7 +191,7 @@ def _xml_files(self): name='author_year_of_birth', display_name='Author year of birth', description='Year in which the author(s) was(/were) born', - extractor=author_single_value_extractor('jaar_geboren'), + extractor=utils.author_single_value_extractor('jaar_geboren'), es_mapping=text_mapping(), ) @@ -200,7 +199,7 @@ def _xml_files(self): name='author_year_of_death', display_name='Author year of death', description='Year in which the author(s) died', - extractor=author_single_value_extractor('jaar_overlijden'), + extractor=utils.author_single_value_extractor('jaar_overlijden'), es_mapping=text_mapping(), ) @@ -211,7 +210,7 @@ def _xml_files(self): name='author_place_of_birth', display_name='Author place of birth', description='Place the author(s) was(/were) born', - extractor=author_single_value_extractor('geb_plaats'), + extractor=utils.author_single_value_extractor('geb_plaats'), es_mapping=keyword_mapping(), ) @@ -219,7 +218,7 @@ def _xml_files(self): name='author_place_of_death', display_name='Author place of death', description='Place where the author(s) died', - extractor=author_single_value_extractor('overl_plaats'), + extractor=utils.author_single_value_extractor('overl_plaats'), es_mapping=keyword_mapping(), ) @@ -230,9 +229,9 @@ def _xml_files(self): name='author_gender', display_name='Author gender', description='Gender of the author(s)', - extractor=join_extracted( + extractor=utils.join_extracted( Pass( # use look-up dict to transform values to string - author_extractor('vrouw'), + utils.author_extractor('vrouw'), transform=lambda values: map( lambda gender: {'0': 'man/unknown', '1': 'woman'}.get(gender, None), values @@ -258,7 +257,7 @@ def _xml_files(self): name='genre', display_name='Genre', description='Genre of the book', - extractor=join_extracted(Metadata('genre')), + extractor=utils.join_extracted(Metadata('genre')), es_mapping=keyword_mapping(), search_filter=MultipleChoiceFilter( description='Select books in these genres', @@ -272,7 +271,7 @@ def _xml_files(self): description='Language in which the book is written', # this extractor is similar to language_code below, # but designed to accept multiple values in case of uncertainty - extractor=join_extracted( + extractor=utils.join_extracted( Backup( XML( # get the language on chapter-level if available attribute='lang', @@ -290,7 +289,7 @@ def _xml_files(self): multiple=True, attribute='id' ), - transform = lambda codes: map(language_name, codes) if codes else None, + transform = lambda codes: map(utils.language_name, codes) if codes else None, ) ), es_mapping=keyword_mapping(), @@ -321,9 +320,9 @@ def _xml_files(self): toplevel=True, recursive=True, ), - transform=single_language_code, + transform=utils.single_language_code, ), - transform=standardize_language_code, + transform=utils.standardize_language_code, ), es_mapping=keyword_mapping(), ) @@ -338,7 +337,7 @@ def _xml_files(self): flatten=True, ), XML( - tag=LINE_TAG, + tag=utils.LINE_TAG, recursive=True, flatten=True, ) @@ -370,11 +369,11 @@ def _xml_files(self): search_field_core=True, csv_core=True, extractor=XML( - tag=LINE_TAG, + tag=utils.LINE_TAG, recursive=True, multiple=True, flatten=True, - transform_soup_func=pad_content, + transform_soup_func=utils.pad_content, ), es_mapping=main_content_mapping(token_counts=True), visualizations=['wordcloud', 'ngram'], From c29eb7873e22505e6e70938af8735c2306e647fb Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 11:21:19 +0100 Subject: [PATCH 058/262] set up query_model_to_es_query and test cases --- backend/api/query_model_to_es_query.py | 4 ++ .../api/tests/test_query_model_to_es_query.py | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 backend/api/query_model_to_es_query.py create mode 100644 backend/api/tests/test_query_model_to_es_query.py diff --git a/backend/api/query_model_to_es_query.py b/backend/api/query_model_to_es_query.py new file mode 100644 index 000000000..28497fe79 --- /dev/null +++ b/backend/api/query_model_to_es_query.py @@ -0,0 +1,4 @@ +# converts json of the frontend 'QueryModel' to an elasticsearch query. + +def query_model_to_es_query(query_model): + return query_model diff --git a/backend/api/tests/test_query_model_to_es_query.py b/backend/api/tests/test_query_model_to_es_query.py new file mode 100644 index 000000000..67eda6692 --- /dev/null +++ b/backend/api/tests/test_query_model_to_es_query.py @@ -0,0 +1,41 @@ +import pytest +from api.query_model_to_es_query import query_model_to_es_query + +cases = [ + ( + 'blank search', + {'queryText': None, 'filters': [], 'sortAscending': True}, + {'query': {'bool': {'must': {'match_all': {}}, 'filter': []}}} + ), ( + 'query text, no filters', + {'queryText':'test','filters':[],'sortAscending':True}, + {'query':{'bool':{'must':{'simple_query_string':{'query':'test','lenient':True,'default_operator':'or'}},'filter':[]}}} + ), ( + 'search fields', + {'queryText':'test','filters':[],'sortAscending':True,'fields':['content']}, + {'query':{'bool':{'must':{'simple_query_string':{'query':'test','lenient':True,'default_operator':'or','fields':['content']}},'filter':[]}}} + ), ( + 'date filter', + {'queryText':None,'filters':[{'fieldName':'date','description':'Search only within this time range.','useAsFilter':True,'defaultData':{'filterType':'DateFilter','min':'1815-01-01','max':'2022-12-31'},'currentData':{'filterType':'DateFilter','min':'1900-01-01','max':'2000-12-31'}}]}, + {'query':{'bool':{'must':{'match_all':{}},'filter':[{'range':{'date':{'gte':'1900-01-01','lte':'2000-12-31','format':'yyyy-MM-dd'}}}]}}}, + ), ( + 'terms filter', + {'queryText':None,'filters':[{'fieldName':'chamber','description':'Search only in debates from the selected chamber(s)','useAsFilter':True,'defaultData':{'filterType':'MultipleChoiceFilter','optionCount':2,'selected':[]},'currentData':{'filterType':'MultipleChoiceFilter','selected':['Eerste%20Kamer']}}]}, + {'query':{'bool':{'must':{'match_all':{}},'filter':[{'terms':{'chamber':['Eerste Kamer']}}]}}}, + ), ( + 'sort by field', + {'queryText':None,'filters':[],'sortBy':'date','sortAscending':True}, + {'query':{'bool':{'must':{'match_all':{}},'filter':[]}},'sort':[{'date':'asc'}]}, + ), ( + 'highlight', + {'queryText':'test','filters':[],'sortAscending':True,'highlight':10}, + {'query':{'bool':{'must':{'simple_query_string':{'query':'test','lenient':True,'default_operator':'or'}},'filter':[]}},'highlight':{'fragment_size':10,'pre_tags':[''],'post_tags':[''],'order':'score','fields':[{'id':{}},{'title':{}},{'content':{}}]}} + ) +] + +def get_name(case): return case[0] + +@pytest.mark.parametrize('name,query_model,es_query', cases, ids=map(get_name, cases)) +def test_query_model_to_es_query(name, query_model, es_query): + result = query_model_to_es_query(query_model) + assert result == es_query From fcca3f192c0b49b7c9380f211cc2611bea04da95 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 11:57:07 +0100 Subject: [PATCH 059/262] querymodel -> esquery conversion: querytext --- backend/api/query_model_to_es_query.py | 5 +++- backend/visualization/query.py | 34 ++++++++++++++------------ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/backend/api/query_model_to_es_query.py b/backend/api/query_model_to_es_query.py index 28497fe79..039ac0fb2 100644 --- a/backend/api/query_model_to_es_query.py +++ b/backend/api/query_model_to_es_query.py @@ -1,4 +1,7 @@ # converts json of the frontend 'QueryModel' to an elasticsearch query. +from visualization import query + def query_model_to_es_query(query_model): - return query_model + es_query = query.set_query_text(query.MATCH_ALL, query_model['queryText']) + return es_query diff --git a/backend/visualization/query.py b/backend/visualization/query.py index af0b940eb..018640e7b 100644 --- a/backend/visualization/query.py +++ b/backend/visualization/query.py @@ -18,31 +18,35 @@ def set_query_text(query, text): if get_query_text(query): new_query['query']['bool']['must']['simple_query_string']['query'] = text - elif query['query']['bool']['must']: - new_query['query']['bool']['must'] = { - "simple_query_string": { - "query": text, - "lenient": True, - "default_operator": "or" - } - } + elif 'bool' in query['query'] and query['query']['bool']['must']: + new_query['query']['bool']['must'] = format_query_text(text) else: new_query['query'] ={ "bool": { - "must": { - "simple_query_string": { - "query": text, - "lenient": True, - "default_operator": "or" - } - }, + "must": format_query_text(text), "filter": [] } } return new_query +def format_query_text(query_text = None): + '''Render the portion of the query that specifies the query text. Either simple_query_string, + or match_all if the query text is None.''' + + if query_text: + return {'simple_query_string': + { + 'query': query_text, + 'lenient': True, + 'default_operator':'or' + } + } + else: + return {'match_all': {}} + + def get_search_fields(query): """Get the search fields specified in the query.""" try: From d5373b90d0b320e0ea757b77f856c76587e91a14 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 12:02:12 +0100 Subject: [PATCH 060/262] querymodel -> esquery conversion: search fields --- backend/api/query_model_to_es_query.py | 13 ++++++++++++- backend/visualization/query.py | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/api/query_model_to_es_query.py b/backend/api/query_model_to_es_query.py index 039ac0fb2..2e71cfa5f 100644 --- a/backend/api/query_model_to_es_query.py +++ b/backend/api/query_model_to_es_query.py @@ -3,5 +3,16 @@ from visualization import query def query_model_to_es_query(query_model): - es_query = query.set_query_text(query.MATCH_ALL, query_model['queryText']) + es_query = query.set_query_text(query.MATCH_ALL, get_query_text(query_model)) + + search_fields = get_search_fields(query_model) + if search_fields: + es_query = query.set_search_fields(es_query, search_fields) + return es_query + +def get_query_text(query_model): + return query_model['queryText'] + +def get_search_fields(query_model): + return query_model.get('fields', None) diff --git a/backend/visualization/query.py b/backend/visualization/query.py index 018640e7b..2e8ca6d4c 100644 --- a/backend/visualization/query.py +++ b/backend/visualization/query.py @@ -56,6 +56,15 @@ def get_search_fields(query): return fields +def set_search_fields(query, fields): + '''Set the search fields for a query''' + + if get_query_text(query) == None: + return query + else: + query['query']['bool']['must']['simple_query_string']['fields'] = fields + return query + def get_filters(query): """Get the list of filters in a query, or `None` if there are none.""" try: From 69605e751b98c109e700f43dd5f6d79ebf2a3f30 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 12:10:17 +0100 Subject: [PATCH 061/262] querymodel -> esquery conversion: sorting --- backend/api/query_model_to_es_query.py | 10 ++++++++++ backend/visualization/query.py | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/backend/api/query_model_to_es_query.py b/backend/api/query_model_to_es_query.py index 2e71cfa5f..26d152d57 100644 --- a/backend/api/query_model_to_es_query.py +++ b/backend/api/query_model_to_es_query.py @@ -9,6 +9,10 @@ def query_model_to_es_query(query_model): if search_fields: es_query = query.set_search_fields(es_query, search_fields) + sort_by, sort_direction = get_sort(query_model) + if sort_by: + es_query = query.set_sort(es_query, sort_by, sort_direction) + return es_query def get_query_text(query_model): @@ -16,3 +20,9 @@ def get_query_text(query_model): def get_search_fields(query_model): return query_model.get('fields', None) + +def get_sort(query_model): + sort_by = query_model.get('sortBy', None) + direction = 'asc' if query_model.get('sortAscending', True) else 'desc' + return sort_by, direction + diff --git a/backend/visualization/query.py b/backend/visualization/query.py index 2e8ca6d4c..190b228c8 100644 --- a/backend/visualization/query.py +++ b/backend/visualization/query.py @@ -134,6 +134,17 @@ def make_term_filter(field, value): } } +def set_sort(query, sort_by, sort_direction): + '''sets the 'sort' specification for a query. + Parameters: + - `query`: elasticsearch query + - `sort_by`: string; the name of the field by which you want to sort + - `direction`: either `'asc'` or `'desc'` + ''' + specification = [{sort_by:sort_direction}] + query['sort'] = specification + return query + def remove_query(query): """ Remove the query part of the query object From f0416227db37d72ae2a576d3adc2c5d357c78570 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 12:17:25 +0100 Subject: [PATCH 062/262] querymodel -> esquery conversion: highlight --- backend/api/query_model_to_es_query.py | 9 ++++++++- backend/api/tests/test_query_model_to_es_query.py | 2 +- backend/visualization/query.py | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/api/query_model_to_es_query.py b/backend/api/query_model_to_es_query.py index 26d152d57..4f7fd350e 100644 --- a/backend/api/query_model_to_es_query.py +++ b/backend/api/query_model_to_es_query.py @@ -13,10 +13,14 @@ def query_model_to_es_query(query_model): if sort_by: es_query = query.set_sort(es_query, sort_by, sort_direction) + highlight = get_highlight(query_model) + if highlight: + es_query = query.set_highlight(es_query, highlight) + return es_query def get_query_text(query_model): - return query_model['queryText'] + return query_model.get('queryText', None) def get_search_fields(query_model): return query_model.get('fields', None) @@ -26,3 +30,6 @@ def get_sort(query_model): direction = 'asc' if query_model.get('sortAscending', True) else 'desc' return sort_by, direction +def get_highlight(query_model): + return query_model.get('highlight', None) + diff --git a/backend/api/tests/test_query_model_to_es_query.py b/backend/api/tests/test_query_model_to_es_query.py index 67eda6692..fd1f8fdfb 100644 --- a/backend/api/tests/test_query_model_to_es_query.py +++ b/backend/api/tests/test_query_model_to_es_query.py @@ -29,7 +29,7 @@ ), ( 'highlight', {'queryText':'test','filters':[],'sortAscending':True,'highlight':10}, - {'query':{'bool':{'must':{'simple_query_string':{'query':'test','lenient':True,'default_operator':'or'}},'filter':[]}},'highlight':{'fragment_size':10,'pre_tags':[''],'post_tags':[''],'order':'score','fields':[{'id':{}},{'title':{}},{'content':{}}]}} + {'query':{'bool':{'must':{'simple_query_string':{'query':'test','lenient':True,'default_operator':'or'}},'filter':[]}},'highlight':{'fragment_size':10}} ) ] diff --git a/backend/visualization/query.py b/backend/visualization/query.py index 190b228c8..790463d7c 100644 --- a/backend/visualization/query.py +++ b/backend/visualization/query.py @@ -145,6 +145,11 @@ def set_sort(query, sort_by, sort_direction): query['sort'] = specification return query +def set_highlight(query, fragment_size): + specification = { 'fragment_size': fragment_size } + query['highlight'] = specification + return query + def remove_query(query): """ Remove the query part of the query object From 7c88d4912094e492bd1b19e5184472d462a4416c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 16:34:24 +0100 Subject: [PATCH 063/262] querymodel -> esquery conversion: filters --- backend/api/query_model_to_es_query.py | 66 +++++++++++++++++++ .../api/tests/test_query_model_to_es_query.py | 8 +++ 2 files changed, 74 insertions(+) diff --git a/backend/api/query_model_to_es_query.py b/backend/api/query_model_to_es_query.py index 4f7fd350e..a1c96695d 100644 --- a/backend/api/query_model_to_es_query.py +++ b/backend/api/query_model_to_es_query.py @@ -1,10 +1,15 @@ # converts json of the frontend 'QueryModel' to an elasticsearch query. from visualization import query +from urllib.parse import unquote def query_model_to_es_query(query_model): es_query = query.set_query_text(query.MATCH_ALL, get_query_text(query_model)) + filters = get_filters(query_model) + for filter in filters: + es_query = query.add_filter(es_query, filter) + search_fields = get_search_fields(query_model) if search_fields: es_query = query.set_search_fields(es_query, search_fields) @@ -33,3 +38,64 @@ def get_sort(query_model): def get_highlight(query_model): return query_model.get('highlight', None) +def get_filters(query_model): + return [ + convert_filter(filter) + for filter in query_model.get('filters', []) + if filter.get('useAsFilter', False) + ] + +def convert_filter(filter): + field = filter['fieldName'] + type = filter['currentData']['filterType'] + + type_converters = { + 'DateFilter': convert_date_filter, + 'RangeFilter': convert_range_filter, + 'MultipleChoiceFilter': convert_terms_filter, + 'BooleanFilter': convert_boolean_filter, + } + + return type_converters[type](field, filter['currentData']) + +def convert_date_filter(field, data): + min = data['min'] + max = data['max'] + + return { + 'range': { + field: { + 'gte': min, + 'lte': max, + 'format':'yyyy-MM-dd', + } + } + } + +def convert_range_filter(field, data): + min = data['min'] + max = data['max'] + + return { + 'range': { + field: { + 'gte': min, + 'lte': max, + } + } + } + +def convert_terms_filter(field, data): + selected = data['selected'] + decoded = list(map(unquote, selected)) + + return { + 'terms': {field: decoded} + } + +def convert_boolean_filter(field, data): + checked = data['checked'] + + return { + 'term': {field: checked} + } diff --git a/backend/api/tests/test_query_model_to_es_query.py b/backend/api/tests/test_query_model_to_es_query.py index fd1f8fdfb..a1ea78c00 100644 --- a/backend/api/tests/test_query_model_to_es_query.py +++ b/backend/api/tests/test_query_model_to_es_query.py @@ -18,11 +18,19 @@ 'date filter', {'queryText':None,'filters':[{'fieldName':'date','description':'Search only within this time range.','useAsFilter':True,'defaultData':{'filterType':'DateFilter','min':'1815-01-01','max':'2022-12-31'},'currentData':{'filterType':'DateFilter','min':'1900-01-01','max':'2000-12-31'}}]}, {'query':{'bool':{'must':{'match_all':{}},'filter':[{'range':{'date':{'gte':'1900-01-01','lte':'2000-12-31','format':'yyyy-MM-dd'}}}]}}}, + ), ( + 'range filter', + {'queryText':None,'filters':[{'fieldName':'year','description':'Restrict the years from which search results will be returned.','useAsFilter':True,'defaultData':{'filterType':'RangeFilter','min':1957,'max':2008},'currentData':{'filterType':'RangeFilter','min':1967,'max':1989}}],'sortAscending':True}, + {'query':{'bool':{'must':{'match_all':{}},'filter':[{'range':{'year':{'gte':1967,'lte':1989}}}]}}} ), ( 'terms filter', {'queryText':None,'filters':[{'fieldName':'chamber','description':'Search only in debates from the selected chamber(s)','useAsFilter':True,'defaultData':{'filterType':'MultipleChoiceFilter','optionCount':2,'selected':[]},'currentData':{'filterType':'MultipleChoiceFilter','selected':['Eerste%20Kamer']}}]}, {'query':{'bool':{'must':{'match_all':{}},'filter':[{'terms':{'chamber':['Eerste Kamer']}}]}}}, ), ( + 'boolean filter', + {'queryText':None,'filters':[{'fieldName':'has_content','description':'Accept only articles that have available text content.','useAsFilter':True,'defaultData':{'filterType':'BooleanFilter','checked':False},'currentData':{'filterType':'BooleanFilter','checked':True}}],'sortBy':'date','sortAscending':True}, + {'query':{'bool':{'must':{'match_all':{}},'filter':[{'term':{'has_content':True}}]}},'sort':[{'date':'asc'}]} + ),( 'sort by field', {'queryText':None,'filters':[],'sortBy':'date','sortAscending':True}, {'query':{'bool':{'must':{'match_all':{}},'filter':[]}},'sort':[{'date':'asc'}]}, From 386994ccf82d1213231a38f27df3a459f41685f1 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 17:41:19 +0100 Subject: [PATCH 064/262] add reverse functions --- backend/api/es_query_to_query_model.py | 102 ++++++++++++++++++ .../api/tests/test_query_model_to_es_query.py | 19 ++++ 2 files changed, 121 insertions(+) create mode 100644 backend/api/es_query_to_query_model.py diff --git a/backend/api/es_query_to_query_model.py b/backend/api/es_query_to_query_model.py new file mode 100644 index 000000000..b22df9fdc --- /dev/null +++ b/backend/api/es_query_to_query_model.py @@ -0,0 +1,102 @@ +from visualization import query +from urllib.parse import quote + +def es_query_to_query_model(es_query): + model = dict() + + transformations = [ + include_query_text, + include_search_fields, + include_filters, + include_sort, + include_highlight + ] + + for transform in transformations: + transform(model, es_query) + + return model + +def include_query_text(model, es_query): + query_text = query.get_query_text(es_query) + model['queryText'] = query_text + return model + +def include_search_fields(model, es_query): + search_fields = query.get_search_fields(es_query) + if search_fields: + model['fields'] = search_fields + return model + +def include_sort(model, es_query): + if 'sort' in es_query: + sort = es_query['sort'][0] + field = list(sort.keys())[0] + direction = sort[field] + ascending = direction == 'asc' + model['sortBy'] = field + model['sortAscending'] = ascending + + return model + +def include_highlight(model, es_query): + if 'highlight' in es_query: + higlight = es_query['highlight'] + size = higlight['fragment_size'] + model['highlight'] = size + return model + +def include_filters(model, es_query): + filters = query.get_filters(es_query) + model['filters'] = list(map(format_filter_for_query_model, filters)) + return model + +def format_filter_for_query_model(es_filter): + type = list(es_filter.keys())[0] + condition = es_filter[type] + field = list(condition.keys())[0] + + data_formatters = { + 'range': format_range_data, + 'terms': format_terms_data, + 'term': format_term_data, + } + + current_data = data_formatters[type](condition[field]) + + return { + 'fieldName': field, + 'description': '', + 'useAsFilter': True, + 'currentData': current_data, + } + +def format_range_data(data): + min = data['gte'] + max = data['lte'] + + if data.get('format', None): + return { + 'filterType': 'DateFilter', + 'min': min, + 'max': max, + } + + return { + 'filterType': 'RangeFilter', + 'min': min, + 'max': max, + } + +def format_term_data(data): + return { + 'filterType': 'BooleanFilter', + 'checked': data + } + +def format_terms_data(data): + selected = list(map(quote, data)) + return { + 'filterType': 'MultipleChoiceFilter', + 'selected': selected + } diff --git a/backend/api/tests/test_query_model_to_es_query.py b/backend/api/tests/test_query_model_to_es_query.py index a1ea78c00..bca59fa36 100644 --- a/backend/api/tests/test_query_model_to_es_query.py +++ b/backend/api/tests/test_query_model_to_es_query.py @@ -1,5 +1,7 @@ import pytest from api.query_model_to_es_query import query_model_to_es_query +from api.es_query_to_query_model import es_query_to_query_model +from copy import deepcopy cases = [ ( @@ -47,3 +49,20 @@ def get_name(case): return case[0] def test_query_model_to_es_query(name, query_model, es_query): result = query_model_to_es_query(query_model) assert result == es_query + +@pytest.mark.parametrize('name,query_model,es_query', cases, ids=map(get_name, cases)) +def test_es_query_to_query_model(name, query_model, es_query): + result = es_query_to_query_model(es_query) + + # clean up the model to remove some data that is irrelevant for querying + # and thus not represented in es_query + # it's not relevant for the search history either, so we don't need it + + model_copy = deepcopy(query_model) + if 'sortBy' not in model_copy and 'sortAscending' in model_copy: + del model_copy['sortAscending'] + for filter in model_copy['filters']: + filter['description'] = '' + del filter['defaultData'] + + assert result == model_copy From 6b016c33a514eb36fbefc9a0d292df1ee233d760 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 17:47:16 +0100 Subject: [PATCH 065/262] data migration to convert to query model --- .../migrations/0002_convert_to_es_query.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 backend/api/migrations/0002_convert_to_es_query.py diff --git a/backend/api/migrations/0002_convert_to_es_query.py b/backend/api/migrations/0002_convert_to_es_query.py new file mode 100644 index 000000000..04b3142cd --- /dev/null +++ b/backend/api/migrations/0002_convert_to_es_query.py @@ -0,0 +1,32 @@ +# Generated by Django 4.1.5 on 2023-03-13 15:46 + +from django.db import migrations +from api.query_model_to_es_query import query_model_to_es_query +from api.es_query_to_query_model import es_query_to_query_model + +def convert_query_format_to_es_query(apps, schema_editor): + Query = apps.get_model('api', 'Query') + queries = Query.objects.all() + for query in queries: + query.query_json = query_model_to_es_query(query.query_json) + query.save() + +def convert_query_format_to_query_model(apps, schema_editor): + Query = apps.get_model('api', 'Query') + queries = Query.objects.all() + for query in queries: + query.query_json = es_query_to_query_model(query.query_json) + query.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.RunPython( + code=convert_query_format_to_es_query, + reverse_code=convert_query_format_to_query_model, + ) + ] From 1e8215f627b1ad0c0d40b84d9b752f158d14d555 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 18:04:08 +0100 Subject: [PATCH 066/262] query conversion when importing legacy data --- backend/ianalyzer/flask_data_transfer.py | 5 ++++- backend/ianalyzer/tests/test_flask_data_transfer.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/ianalyzer/flask_data_transfer.py b/backend/ianalyzer/flask_data_transfer.py index f438ce872..ca402c4ad 100644 --- a/backend/ianalyzer/flask_data_transfer.py +++ b/backend/ianalyzer/flask_data_transfer.py @@ -11,6 +11,7 @@ from django.conf import settings import warnings from allauth.account.models import EmailAddress +from api.query_model_to_es_query import query_model_to_es_query def adapt_password_encoding(flask_encoded): @@ -149,9 +150,11 @@ def save_flask_query(row): # some queries refer to corpus names that no longer exist return + query_model = json.loads(row['query']) + es_query = query_model_to_es_query(query_model) query = Query( id=row['id'], - query_json=load_json_value(row['query']), + query_json=es_query, corpus=Corpus.objects.get(name=corpus_name), user=CustomUser.objects.get(id=user_id), completed=null_to_none(row['completed']), diff --git a/backend/ianalyzer/tests/test_flask_data_transfer.py b/backend/ianalyzer/tests/test_flask_data_transfer.py index 709fc7c6e..3b20f8591 100644 --- a/backend/ianalyzer/tests/test_flask_data_transfer.py +++ b/backend/ianalyzer/tests/test_flask_data_transfer.py @@ -119,7 +119,10 @@ def test_save_queries(db): query = Query.objects.get(id='507') assert query.query_json == { - "queryText": "", "filters": [], "sortBy": "date", "sortAscending": False} + "sort": [{"date": "desc"}], + "query": {"bool": {"must": {"match_all": {}}, "filter": []}} + } + assert dates_match(query.started, datetime(year=2022, month=12, day=7, hour=14, minute=18, second=6)) From 3096539c27f4a11d3cb6029e40007397783dc6c8 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 18:19:57 +0100 Subject: [PATCH 067/262] adapt frontend to database model --- .../search-history/search-history.component.html | 4 ++-- .../search-history/search-history.component.ts | 15 +++++++++++---- frontend/src/app/models/query.ts | 8 +++++--- .../src/app/services/elastic-search.service.ts | 6 +++--- frontend/src/app/services/search.service.ts | 3 ++- frontend/src/mock-data/elastic-search.ts | 3 ++- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/history/search-history/search-history.component.html b/frontend/src/app/history/search-history/search-history.component.html index e6a731dd5..85efdf7f4 100644 --- a/frontend/src/app/history/search-history/search-history.component.html +++ b/frontend/src/app/history/search-history/search-history.component.html @@ -27,8 +27,8 @@ {{query.started | date:'medium'}} - {{query.query_json | formatQueryText }} - + {{query.queryModel | formatQueryText }} + {{query.total_results}} {{corpusTitle(query.corpus)}} diff --git a/frontend/src/app/history/search-history/search-history.component.ts b/frontend/src/app/history/search-history/search-history.component.ts index e9eb43398..01f413c3c 100644 --- a/frontend/src/app/history/search-history/search-history.component.ts +++ b/frontend/src/app/history/search-history/search-history.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import * as _ from 'lodash'; import { QueryDb } from '../../models/index'; -import { CorpusService, SearchService, QueryService, ParamService } from '../../services/index'; +import { CorpusService, SearchService, QueryService, ParamService, ElasticSearchService } from '../../services/index'; import { HistoryDirective } from '../history.directive'; @Component({ @@ -17,7 +17,8 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { private paramService: ParamService, corpusService: CorpusService, private queryService: QueryService, - private router: Router + private router: Router, + private elasticSearchService: ElasticSearchService, ) { super(corpusService); } @@ -28,12 +29,18 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { searchHistory => { const sortedQueries = this.sortByDate(searchHistory); // not using _.sortedUniqBy as sorting and filtering takes place w/ different aspects - this.queries = _.uniqBy(sortedQueries, query => query.query_json); + this.queries = _.uniqBy(sortedQueries, query => query.query_json).map(this.addQueryModel.bind(this)); }); } + addQueryModel(query?: QueryDb) { + const corpus = this.corpora.find(c => c.name === query.corpus); + query.queryModel = this.elasticSearchService.esQueryToQueryModel(query.query_json, corpus); + return query; + } + returnToSavedQuery(query: QueryDb) { - const route = this.paramService.queryModelToRoute(query.query_json); + const route = this.paramService.queryModelToRoute(query.queryModel); this.router.navigate(['/search', query.corpus, route]); if (window) { window.scrollTo(0, 0); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index a1323c953..0e831137a 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,10 +1,11 @@ import {SearchFilter } from '../models/index'; +import { EsQuery } from '../services'; import { SearchFilterData } from './search-filter'; /** This is the query object as it is saved in the database.*/ export class QueryDb { constructor( - query: QueryModel, + esQuery: EsQuery, /** * Name of the corpus for which the query was performed. */ @@ -14,7 +15,7 @@ export class QueryDb { * User that performed this query. */ public user: number) { - this.query_json = query; + this.query_json = esQuery; } /** @@ -25,7 +26,8 @@ export class QueryDb { /** * JSON string representing the query model (i.e., query text and filters, see below). */ - public query_json: QueryModel; + public query_json: EsQuery; + queryModel?: QueryModel; /** * Time the first document was sent. diff --git a/frontend/src/app/services/elastic-search.service.ts b/frontend/src/app/services/elastic-search.service.ts index 299b14c64..b9a69f7a9 100644 --- a/frontend/src/app/services/elastic-search.service.ts +++ b/frontend/src/app/services/elastic-search.service.ts @@ -52,7 +52,7 @@ export class ElasticSearchService { if (filters.length) { return { queryText, filters }; } else { - return { queryText }; + return { queryText, filters: [] }; } } @@ -112,10 +112,10 @@ export class ElasticSearchService { const filterData = searchFilterDataFromField(field, value); return { fieldName: field.name, - description: field.searchFilter.description, + description: field.searchFilter?.description || '', useAsFilter: true, currentData: filterData, - defaultData: field.searchFilter.defaultData, + defaultData: field.searchFilter?.defaultData, }; } diff --git a/frontend/src/app/services/search.service.ts b/frontend/src/app/services/search.service.ts index 9c6a9443c..8a005a047 100644 --- a/frontend/src/app/services/search.service.ts +++ b/frontend/src/app/services/search.service.ts @@ -81,7 +81,8 @@ export class SearchService { corpus: Corpus ): Promise { const user = await this.authService.getCurrentUserPromise(); - const query = new QueryDb(queryModel, corpus.name, user.id); + const esQuery = this.elasticSearchService.makeEsQuery(queryModel, corpus.fields); + const query = new QueryDb(esQuery, corpus.name, user.id); query.started = new Date(Date.now()); const results = await this.elasticSearchService.search( corpus, diff --git a/frontend/src/mock-data/elastic-search.ts b/frontend/src/mock-data/elastic-search.ts index 26b2cef33..4a45c2061 100644 --- a/frontend/src/mock-data/elastic-search.ts +++ b/frontend/src/mock-data/elastic-search.ts @@ -10,7 +10,8 @@ export class ElasticSearchServiceMock { esQueryToQueryModel(query: EsQuery, corpus: Corpus): QueryModel { return { - queryText: '' + queryText: '', + filters: [] }; } From 62269b84091b598129cc0384dae1fdbb02b25c27 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 13 Mar 2023 18:37:05 +0100 Subject: [PATCH 068/262] update frontend test --- frontend/src/app/services/elastic-search.service.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/services/elastic-search.service.spec.ts b/frontend/src/app/services/elastic-search.service.spec.ts index b4179798f..b442105c1 100644 --- a/frontend/src/app/services/elastic-search.service.spec.ts +++ b/frontend/src/app/services/elastic-search.service.spec.ts @@ -83,7 +83,8 @@ describe('ElasticSearchService', () => { const querymodels: QueryModel[] = [ { - queryText: 'test' + queryText: 'test', + filters: [], }, { queryText: 'test', From 3c575d068c1bd36360c335b62f32dc4d90480c7b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 12:57:09 +0100 Subject: [PATCH 069/262] rename old search-filter component --- frontend/src/app/models/corpus.ts | 2 +- frontend/src/app/models/index.ts | 2 +- frontend/src/app/models/query.ts | 2 +- .../src/app/models/{search-filter.ts => search-filter-old.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename frontend/src/app/models/{search-filter.ts => search-filter-old.ts} (100%) diff --git a/frontend/src/app/models/corpus.ts b/frontend/src/app/models/corpus.ts index 3c3c361dd..09aad7991 100644 --- a/frontend/src/app/models/corpus.ts +++ b/frontend/src/app/models/corpus.ts @@ -1,4 +1,4 @@ -import { SearchFilter, SearchFilterData } from './search-filter'; +import { SearchFilter, SearchFilterData } from './search-filter-old'; // corresponds to the corpus definition on the backend. export class Corpus implements ElasticSearchIndex { diff --git a/frontend/src/app/models/index.ts b/frontend/src/app/models/index.ts index 378c9acca..42f7c24da 100644 --- a/frontend/src/app/models/index.ts +++ b/frontend/src/app/models/index.ts @@ -1,7 +1,7 @@ export * from './corpus'; export * from './found-document'; export * from './query'; -export * from './search-filter'; +export * from './search-filter-old'; export * from './search-results'; export * from './sort-event'; export * from './user'; diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 0e831137a..64d570262 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,6 +1,6 @@ import {SearchFilter } from '../models/index'; import { EsQuery } from '../services'; -import { SearchFilterData } from './search-filter'; +import { SearchFilterData } from './search-filter-old'; /** This is the query object as it is saved in the database.*/ export class QueryDb { diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter-old.ts similarity index 100% rename from frontend/src/app/models/search-filter.ts rename to frontend/src/app/models/search-filter-old.ts From 65653a06e91a9ef03b215f64f9ac04e7fa3f0220 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 13:05:43 +0100 Subject: [PATCH 070/262] add abstract SearchFilter class --- frontend/src/app/models/search-filter.ts | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 frontend/src/app/models/search-filter.ts diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts new file mode 100644 index 000000000..dfeb12a98 --- /dev/null +++ b/frontend/src/app/models/search-filter.ts @@ -0,0 +1,43 @@ +import { BehaviorSubject } from 'rxjs'; +import { CorpusField } from './corpus'; +import { EsFilter } from './elasticsearch'; + +abstract class SearchFilter { + corpusField: CorpusField; + defaultData: FilterData; + data: BehaviorSubject; + + constructor(corpusField: CorpusField) { + this.corpusField = corpusField; + this.defaultData = this.makeDefaultData(corpusField.searchFilter); + this.data = new BehaviorSubject(this.defaultData); + } + + get currentData() { + return this.data?.value; + } + + reset() { + this.data.next(this.defaultData); + } + + abstract makeDefaultData(filterDefinition): FilterData; + + /** + * filter for one specific value (used to find documents from + * the same day, page, publication, etc. as a specific document) + */ + abstract setToValue(value: any): void; + + /** + * set value based on route parameter + */ + abstract setFromParam(param: string): void; + + abstract toRouteParam(): {[param: string]: any}; + + /** + * export as filter specification in elasticsearch query language + */ + abstract toEsFilter(): EsFilter; +} From f4bd90874dab4a71ec989a28c4358b6e524e43cf Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 13:22:02 +0100 Subject: [PATCH 071/262] add search filter models --- frontend/src/app/models/search-filter-old.ts | 2 +- frontend/src/app/models/search-filter.spec.ts | 71 ++++++ frontend/src/app/models/search-filter.ts | 203 ++++++++++++++++-- 3 files changed, 262 insertions(+), 14 deletions(-) create mode 100644 frontend/src/app/models/search-filter.spec.ts diff --git a/frontend/src/app/models/search-filter-old.ts b/frontend/src/app/models/search-filter-old.ts index ce57a82a1..cd3b9606f 100644 --- a/frontend/src/app/models/search-filter-old.ts +++ b/frontend/src/app/models/search-filter-old.ts @@ -75,7 +75,7 @@ export const searchFilterDataFromField = (field: CorpusField, value: string[]): } }; -const parseMinMax = (value: string[]): [string, string] => { +export const parseMinMax = (value: string[]): [string, string] => { const term = value[0]; if (term.split(':').length === 2) { return term.split(':') as [string, string]; diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts new file mode 100644 index 000000000..97ce16859 --- /dev/null +++ b/frontend/src/app/models/search-filter.spec.ts @@ -0,0 +1,71 @@ +import { CorpusField } from './corpus'; +import { DateFilter } from './search-filter'; + +describe('DateFilter', () => { + const mockField: CorpusField = { + name: 'date', + displayName: 'Date', + description: '', + displayType: 'date', + hidden: false, + sortable: true, + primarySort: false, + searchable: false, + downloadable: true, + searchFilter: { + fieldName: 'date', + description: '', + useAsFilter: true, + currentData: { + filterType: 'DateFilter', + min: '1800-01-01', + max: '1899-12-31' + }, + defaultData: { + filterType: 'DateFilter', + min: '1800-01-01', + max: '1899-12-31' + } + }, + mappingType: 'date', + }; + + it('should create', () => { + const filter = new DateFilter(mockField); + expect(filter).toBeTruthy(); + expect(filter.currentData).toEqual({ + min: new Date(Date.parse('Jan 01 1800')), + max: new Date(Date.parse('Dec 31 1899')) + }); + }); + + it('should convert to string', () => { + const filter = new DateFilter(mockField); + const dataAsString = filter.dataToString(filter.currentData); + expect(filter.dataFromString(dataAsString)).toEqual(filter.currentData); + }); + + it('should set data from a value', () => { + const filter = new DateFilter(mockField); + const date = new Date(Date.parse('Jan 01 1850')); + filter.setToValue(date); + expect(filter.currentData).toEqual({ + min: date, + max: date, + }); + }); + + it('should convert to an elasticsearch filter', () => { + const filter = new DateFilter(mockField); + const esFilter = filter.toEsFilter(); + expect(esFilter).toEqual({ + range: { + date: { + gte: '1800-01-01', + lte: '1899-12-31', + format: 'yyyy-MM-dd' + } + } + }); + }); +}); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index dfeb12a98..c8cafa530 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -1,6 +1,7 @@ +import * as moment from 'moment'; import { BehaviorSubject } from 'rxjs'; import { CorpusField } from './corpus'; -import { EsFilter } from './elasticsearch'; +import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter } from './elasticsearch'; abstract class SearchFilter { corpusField: CorpusField; @@ -9,7 +10,7 @@ abstract class SearchFilter { constructor(corpusField: CorpusField) { this.corpusField = corpusField; - this.defaultData = this.makeDefaultData(corpusField.searchFilter); + this.defaultData = this.makeDefaultData(corpusField.searchFilter.defaultData); this.data = new BehaviorSubject(this.defaultData); } @@ -21,23 +22,199 @@ abstract class SearchFilter { this.data.next(this.defaultData); } - abstract makeDefaultData(filterDefinition): FilterData; + /** + * set value based on route parameter + */ + setFromParam(param: string): void { + this.data.next(this.dataFromString(param)); + } - /** - * filter for one specific value (used to find documents from - * the same day, page, publication, etc. as a specific document) - */ - abstract setToValue(value: any): void; + /** + * filter for one specific value (used to find documents from + * the same day, page, publication, etc. as a specific document) + */ + setToValue(value: any) { + this.data.next(this.dataFromValue(value)); + } - /** - * set value based on route parameter - */ - abstract setFromParam(param: string): void; + toRouteParam(): {[param: string]: any} { + return { + [this.corpusField.name]: this.dataToString(this.currentData) + }; + } + + abstract makeDefaultData(filterOptions): FilterData; + + abstract dataFromValue(value: any): FilterData; - abstract toRouteParam(): {[param: string]: any}; + abstract dataFromString(value: string): FilterData; + + abstract dataToString(data: FilterData): string; /** * export as filter specification in elasticsearch query language */ abstract toEsFilter(): EsFilter; } + +interface DateFilterData { + min: Date; + max: Date; +} + +export class DateFilter extends SearchFilter { + makeDefaultData(filterOptions) { + return { + min: this.parseDate(filterOptions.min), + max: this.parseDate(filterOptions.max) + }; + } + + dataFromValue(value: Date) { + return { + min: value, + max: value, + }; + } + + dataFromString(value: string) { + const [minString, maxString] = parseMinMax(value.split(',')); + return { + min: this.parseDate(minString), + max: this.parseDate(maxString), + }; + } + + dataToString(data: DateFilterData) { + const min = this.formatDate(data.min); + const max = this.formatDate(data.max); + return `${min}:${max}`; + } + + toEsFilter(): EsDateFilter { + return { + range: { + [this.corpusField.name]: { + gte: this.formatDate(this.currentData.min), + lte: this.formatDate(this.currentData.max), + format: 'yyyy-MM-dd' + } + } + }; + } + + private formatDate(date: Date): string { + return moment(date).format('YYYY-MM-DD'); + } + + private parseDate(dateString: string): Date { + return moment(dateString, 'YYYY-MM-DD').toDate(); + } +} + +export class BooleanFilter extends SearchFilter { + + makeDefaultData(filterOptions: any) { + return filterOptions.checked; + } + + dataFromValue(value: any): boolean { + return value as boolean; + } + + dataFromString(value: string): boolean { + return value === 'true'; + } + + dataToString(data: boolean): string { + return data.toString(); + } + + toEsFilter(): EsBooleanFilter { + return { + term: { + [this.corpusField.name]: this.currentData + } + }; + } +} + +type MultipleChoiceFilterData = string[]; + +export class MultipleChoiceFilter extends SearchFilter { + makeDefaultData(filterOptions: any): MultipleChoiceFilterData { + return []; + } + + dataFromValue(value: any): MultipleChoiceFilterData { + return [value.toString()]; + } + + dataFromString(value: string): MultipleChoiceFilterData { + return value.split(','); + } + + dataToString(data: MultipleChoiceFilterData): string { + return data.join(','); + } + + toEsFilter(): EsTermsFilter { + return { + terms: { + [this.corpusField.name]: this.currentData + } + }; + } +} + +interface RangeFilterData { + min: number; + max: number; +} + +export class RangeFilter extends SearchFilter { + makeDefaultData(filterOptions: any): RangeFilterData { + return { + min: filterOptions.lower, + max: filterOptions.upper + }; + } + + dataFromValue(value: number): RangeFilterData { + return { min: value, max: value }; + } + + dataFromString(value: string): RangeFilterData { + const [minString, maxString] = parseMinMax(value.split(',')); + return { + min: parseFloat(minString), + max: parseFloat(maxString) + }; + } + + dataToString(data: RangeFilterData): string { + return `${data.min},${data.max}`; + } + + toEsFilter(): EsRangeFilter { + return { + range: { + [this.corpusField.name]: { + gte: this.currentData.min, + lte: this.currentData.max, + } + } + }; + } +} + +const parseMinMax = (value: string[]): [string, string] => { + const term = value[0]; + if (term.split(':').length === 2) { + return term.split(':') as [string, string]; + } else if (value.length === 1) { + return [term, term]; + } else { + return [value[0], value[1]]; + } +}; From 47d9f0ab06584109b974a9cac89f6a160886e432 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 16:26:36 +0100 Subject: [PATCH 072/262] add filteroptions types --- frontend/src/app/models/corpus.ts | 4 +- frontend/src/app/models/elasticsearch.ts | 2 +- .../src/app/models/search-filter-options.ts | 33 ++++++++++++ frontend/src/app/models/search-filter.ts | 40 +++++++++++--- frontend/src/app/services/corpus.service.ts | 54 +------------------ 5 files changed, 70 insertions(+), 63 deletions(-) create mode 100644 frontend/src/app/models/search-filter-options.ts diff --git a/frontend/src/app/models/corpus.ts b/frontend/src/app/models/corpus.ts index 09aad7991..8b74a4ef3 100644 --- a/frontend/src/app/models/corpus.ts +++ b/frontend/src/app/models/corpus.ts @@ -1,4 +1,4 @@ -import { SearchFilter, SearchFilterData } from './search-filter-old'; +import { FilterOptions } from './search-filter-options'; // corresponds to the corpus definition on the backend. export class Corpus implements ElasticSearchIndex { @@ -63,6 +63,6 @@ export interface CorpusField { searchable: boolean; downloadable: boolean; name: string; - searchFilter: SearchFilter | null; + searchFilter: FilterOptions | null; mappingType: 'text' | 'keyword' | 'boolean' | 'date' | 'integer' | null; } diff --git a/frontend/src/app/models/elasticsearch.ts b/frontend/src/app/models/elasticsearch.ts index c1c94b9cf..697fb6060 100644 --- a/frontend/src/app/models/elasticsearch.ts +++ b/frontend/src/app/models/elasticsearch.ts @@ -45,7 +45,7 @@ export interface BooleanQuery { } export interface MatchAll { - match_all: {}; + match_all: Record; } export interface SimpleQueryString { diff --git a/frontend/src/app/models/search-filter-options.ts b/frontend/src/app/models/search-filter-options.ts new file mode 100644 index 000000000..7a874546e --- /dev/null +++ b/frontend/src/app/models/search-filter-options.ts @@ -0,0 +1,33 @@ +// Types for serialised filter options for a corpus by the API + +export interface HasDescription { + description: string; +} + +export type DateFilterOptions = { + name: 'DateFilter'; + lower: string; + upper: string; +} & HasDescription; + +export type MultipleChoiceFilterOptions = { + name: 'MultipleChoiceFilter'; + option_count: number; +} & HasDescription; + +export type RangeFilterOptions = { + name: 'RangeFilter'; + lower: number; + upper: number; +} & HasDescription; + +export type BooleanFilterOptions = { + name: 'BooleanFilter'; + checked: boolean; +} & HasDescription; + +export type FilterOptions = + DateFilterOptions | + MultipleChoiceFilterOptions | + RangeFilterOptions | + BooleanFilterOptions; diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index c8cafa530..2eea86456 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -1,7 +1,9 @@ import * as moment from 'moment'; import { BehaviorSubject } from 'rxjs'; import { CorpusField } from './corpus'; -import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter } from './elasticsearch'; +import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, EsTermFilter } from './elasticsearch'; +import { BooleanFilterOptions, DateFilterOptions, FilterOptions, MultipleChoiceFilterOptions, + RangeFilterOptions } from './search-filter-options'; abstract class SearchFilter { corpusField: CorpusField; @@ -43,7 +45,7 @@ abstract class SearchFilter { }; } - abstract makeDefaultData(filterOptions): FilterData; + abstract makeDefaultData(filterOptions: FilterOptions): FilterData; abstract dataFromValue(value: any): FilterData; @@ -63,7 +65,7 @@ interface DateFilterData { } export class DateFilter extends SearchFilter { - makeDefaultData(filterOptions) { + makeDefaultData(filterOptions: DateFilterOptions) { return { min: this.parseDate(filterOptions.min), max: this.parseDate(filterOptions.max) @@ -114,8 +116,8 @@ export class DateFilter extends SearchFilter { export class BooleanFilter extends SearchFilter { - makeDefaultData(filterOptions: any) { - return filterOptions.checked; + makeDefaultData(filterOptions: BooleanFilterOptions) { + return false; } dataFromValue(value: any): boolean { @@ -142,7 +144,7 @@ export class BooleanFilter extends SearchFilter { type MultipleChoiceFilterData = string[]; export class MultipleChoiceFilter extends SearchFilter { - makeDefaultData(filterOptions: any): MultipleChoiceFilterData { + makeDefaultData(filterOptions: MultipleChoiceFilterOptions): MultipleChoiceFilterData { return []; } @@ -173,7 +175,7 @@ interface RangeFilterData { } export class RangeFilter extends SearchFilter { - makeDefaultData(filterOptions: any): RangeFilterData { + makeDefaultData(filterOptions: RangeFilterOptions): RangeFilterData { return { min: filterOptions.lower, max: filterOptions.upper @@ -208,6 +210,30 @@ export class RangeFilter extends SearchFilter { } } +export class AdHocFilter extends SearchFilter { + makeDefaultData(filterOptions: FilterOptions) {} + + dataFromValue(value: any) { + return value; + } + + dataFromString(value: string) { + return value; + } + + dataToString(data: any): string { + return data.toString(); + } + + toEsFilter(): EsTermFilter { + return { + term: { + [this.corpusField.name]: this.currentData + } + }; + } +} + const parseMinMax = (value: string[]): [string, string] => { const term = value[0]; if (term.split(':').length === 2) { diff --git a/frontend/src/app/services/corpus.service.ts b/frontend/src/app/services/corpus.service.ts index 46ec83410..22cd53d73 100644 --- a/frontend/src/app/services/corpus.service.ts +++ b/frontend/src/app/services/corpus.service.ts @@ -116,55 +116,10 @@ export class CorpusService { searchable: data.searchable, downloadable: data.downloadable, name: data.name, - searchFilter: data['search_filter'] - ? this.parseSearchFilter(data['search_filter'], data['name']) - : null, + searchFilter: data['search_filter'] || null, mappingType: data.es_mapping?.type, }); - private parseSearchFilter( - filter: any, - fieldName: string - ): SearchFilter { - let defaultData: any; - switch (filter.name) { - case 'BooleanFilter': - defaultData = { - filterType: filter.name, - checked: false, - }; - break; - case 'MultipleChoiceFilter': - defaultData = { - filterType: filter.name, - optionCount: filter.option_count, - selected: [], - }; - break; - case 'RangeFilter': - defaultData = { - filterType: filter.name, - min: filter.lower, - max: filter.upper, - }; - break; - case 'DateFilter': - defaultData = { - filterType: filter.name, - min: this.formatDate(new Date(filter.lower)), - max: this.formatDate(new Date(filter.upper)), - }; - break; - } - return { - fieldName, - description: filter.description, - useAsFilter: false, - defaultData, - currentData: defaultData, - }; - } - private parseDate(date: any): Date { // months are zero-based! return new Date( @@ -176,13 +131,6 @@ export class CorpusService { ); } - /** - * Return a string of the form 0123-04-25. - */ - private formatDate(date: Date): string { - return moment(date).format().slice(0, 10); - } - private parseDocumentContext( data: { context_fields: string[] | null; From 52d7b6b660a7058d2dbe343a59274f6a96c7259f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 10 Mar 2023 11:40:20 +0100 Subject: [PATCH 073/262] update corpusfield class --- frontend/src/app/models/corpus.ts | 70 +++++++++++- frontend/src/app/models/search-filter.spec.ts | 37 +----- .../search/search-results.component.spec.ts | 32 ++---- .../src/app/services/corpus.service.spec.ts | 60 ++++------ frontend/src/app/services/corpus.service.ts | 22 +--- frontend/src/mock-data/corpus.ts | 105 ++++++++++++------ 6 files changed, 179 insertions(+), 147 deletions(-) diff --git a/frontend/src/app/models/corpus.ts b/frontend/src/app/models/corpus.ts index 8b74a4ef3..d7d5acfad 100644 --- a/frontend/src/app/models/corpus.ts +++ b/frontend/src/app/models/corpus.ts @@ -1,3 +1,5 @@ +import * as _ from 'lodash'; +import { AdHocFilter, BooleanFilter, DateFilter, MultipleChoiceFilter, RangeFilter, SearchFilter } from './search-filter'; import { FilterOptions } from './search-filter-options'; // corresponds to the corpus definition on the backend. @@ -43,14 +45,38 @@ export interface DocumentContext { displayName: string; } -export interface CorpusField { +export type FieldDisplayType = 'text_content' | 'px' | 'keyword' | 'integer' | 'text' | 'date' | 'boolean'; + +/** Corpus field info as sent by the backend api */ +export interface ApiCorpusField { + name: string; + display_name: string; + display_type: FieldDisplayType; + description: string; + search_filter: FilterOptions | null; + results_overview: boolean; + csv_core: boolean; + search_field_core: boolean; + visualizations: string[]; + visualization_sort: string | null; + es_mapping: any; + indexed: boolean; + hidden: boolean; + required: boolean; + sortable: boolean; + primary_sort: boolean; + searchable: boolean; + downloadable: boolean; +} + +export class CorpusField { description: string; displayName: string; /** * How the field value should be displayed. * text_content: Main text content of the document */ - displayType: 'text_content' | 'px' | 'keyword' | 'integer' | 'text' | 'date' | 'boolean'; + displayType: FieldDisplayType; resultsOverview?: boolean; csvCore?: boolean; searchFieldCore?: boolean; @@ -63,6 +89,44 @@ export interface CorpusField { searchable: boolean; downloadable: boolean; name: string; - searchFilter: FilterOptions | null; + filterOptions: FilterOptions | null; mappingType: 'text' | 'keyword' | 'boolean' | 'date' | 'integer' | null; + + constructor(data: ApiCorpusField) { + this.description = data.description; + this.displayName = data.display_name || data.name; + this.displayType = data.display_type || data['es_mapping']?.type; + this.resultsOverview = data.results_overview; + this.csvCore = data.csv_core; + this.searchFieldCore = data.search_field_core; + this.visualizations = data.visualizations; + this.visualizationSort = data.visualization_sort; + this.multiFields = data['es_mapping']?.fields + ? Object.keys(data['es_mapping'].fields) + : undefined; + this.hidden = data.hidden; + this.sortable = data.sortable; + this.primarySort = data.primary_sort; + this.searchable = data.searchable; + this.downloadable = data.downloadable; + this.name = data.name; + this.filterOptions = data['search_filter'] || null; + this.mappingType = data.es_mapping?.type; + } + + /** make a SearchFilter for this field */ + makeSearchFilter(): SearchFilter { + const filterClasses = { + date: DateFilter, + multiple_choice: MultipleChoiceFilter, + boolean: BooleanFilter, + range: RangeFilter, + }; + const Filter = _.get( + filterClasses, + this.filterOptions?.name, + AdHocFilter + ); + return new Filter(this); + } } diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index 97ce16859..48ba595ff 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -1,37 +1,10 @@ -import { CorpusField } from './corpus'; +import { mockFieldDate } from '../../mock-data/corpus'; import { DateFilter } from './search-filter'; describe('DateFilter', () => { - const mockField: CorpusField = { - name: 'date', - displayName: 'Date', - description: '', - displayType: 'date', - hidden: false, - sortable: true, - primarySort: false, - searchable: false, - downloadable: true, - searchFilter: { - fieldName: 'date', - description: '', - useAsFilter: true, - currentData: { - filterType: 'DateFilter', - min: '1800-01-01', - max: '1899-12-31' - }, - defaultData: { - filterType: 'DateFilter', - min: '1800-01-01', - max: '1899-12-31' - } - }, - mappingType: 'date', - }; it('should create', () => { - const filter = new DateFilter(mockField); + const filter = new DateFilter(mockFieldDate); expect(filter).toBeTruthy(); expect(filter.currentData).toEqual({ min: new Date(Date.parse('Jan 01 1800')), @@ -40,13 +13,13 @@ describe('DateFilter', () => { }); it('should convert to string', () => { - const filter = new DateFilter(mockField); + const filter = new DateFilter(mockFieldDate); const dataAsString = filter.dataToString(filter.currentData); expect(filter.dataFromString(dataAsString)).toEqual(filter.currentData); }); it('should set data from a value', () => { - const filter = new DateFilter(mockField); + const filter = new DateFilter(mockFieldDate); const date = new Date(Date.parse('Jan 01 1850')); filter.setToValue(date); expect(filter.currentData).toEqual({ @@ -56,7 +29,7 @@ describe('DateFilter', () => { }); it('should convert to an elasticsearch filter', () => { - const filter = new DateFilter(mockField); + const filter = new DateFilter(mockFieldDate); const esFilter = filter.toEsFilter(); expect(esFilter).toEqual({ range: { diff --git a/frontend/src/app/search/search-results.component.spec.ts b/frontend/src/app/search/search-results.component.spec.ts index a3c7d8481..0dcfda93e 100644 --- a/frontend/src/app/search/search-results.component.spec.ts +++ b/frontend/src/app/search/search-results.component.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import * as _ from 'lodash'; +import { mockField } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; import { CorpusField } from '../models/index'; @@ -38,38 +40,26 @@ describe('Search Results Component', () => { relation: 'gte' } }; - component.corpus = { + component.corpus = { fields - }; + } as any; component.fromIndex = 0; component.resultsPerPage = 20; fixture.detectChanges(); }); - function createField(name: string): CorpusField { - return { - name, - displayName: name, - description: 'Description', - displayType: 'text', - searchFilter: null, - hidden: false, - sortable: true, - primarySort: false, - searchable: false, - downloadable: true, - mappingType: 'keyword', - }; - } + const createField = (name: string): CorpusField => { + const field = _.cloneDeep(mockField); + field.name = name; + return field; + }; - function createDocument( + const createDocument = ( fieldValues: { [name: string]: string }, id: string, relevance: number, highlight?: {[fieldName: string]: string[]} - ) { - return { id, relevance, fieldValues, highlight }; - } + ) => ({ id, relevance, fieldValues, highlight }); it('should be created', () => { expect(component).toBeTruthy(); diff --git a/frontend/src/app/services/corpus.service.spec.ts b/frontend/src/app/services/corpus.service.spec.ts index 73272f7cf..88155b700 100644 --- a/frontend/src/app/services/corpus.service.spec.ts +++ b/frontend/src/app/services/corpus.service.spec.ts @@ -12,6 +12,7 @@ import { Corpus } from '../models/corpus'; import { CorpusField, SearchFilterData } from '../models/index'; import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; +import * as _ from 'lodash'; describe('CorpusService', () => { let service: CorpusService; @@ -85,7 +86,7 @@ describe('CorpusService', () => { expect(items.map((item) => item.name)).toEqual(['test1', 'test2']); }); - it('should parse filters', () => { + it('should parse fields', () => { apiServiceMock.fakeResult['corpus'] = [ { name: 'times', @@ -200,17 +201,10 @@ describe('CorpusService', () => { ]; return service.get().then((items) => { - const mockMultipleChoiceData: SearchFilterData = { - filterType: 'MultipleChoiceFilter', - optionCount: 42, - selected: [], - }; - const mockRangeData: SearchFilterData = { - filterType: 'RangeFilter', - min: 1785, - max: 2010, - }; - const allFields: CorpusField[] = [ + expect(items.length).toBe(1); + const corpus = _.first(items); + + const fieldData = [ { description: 'Banking concern to which the report belongs.', displayName: 'Bank', @@ -227,12 +221,10 @@ describe('CorpusService', () => { searchable: true, downloadable: false, name: 'bank', - searchFilter: { + filterOptions: { + name: 'MultipleChoiceFilter', description: 'Search only within these banks.', - fieldName: 'bank', - useAsFilter: false, - defaultData: mockMultipleChoiceData, - currentData: mockMultipleChoiceData, + option_count: 42, }, mappingType: 'keyword', }, @@ -252,13 +244,13 @@ describe('CorpusService', () => { visualizations: ['resultscount', 'termfrequency'], visualizationSort: 'key', multiFields: undefined, - searchFilter: { + filterOptions: { description: 'Restrict the years from which search results will be returned.', - fieldName: 'year', - useAsFilter: false, - defaultData: mockRangeData, - currentData: mockRangeData, + name: 'RangeFilter', + lower: 1785, + upper: 2010, + }, mappingType: 'integer', }, @@ -277,27 +269,17 @@ describe('CorpusService', () => { visualizations: ['wordcloud', 'ngram'], visualizationSort: null, multiFields: ['clean', 'stemmed', 'length'], - searchFilter: null, + filterOptions: null, searchFieldCore: true, mappingType: 'text', }, ]; - expect(items).toEqual([ - new Corpus( - 'default', - 'times', - 'Times', - 'This is a description.', - 'times', - allFields, - new Date(1785, 0, 1, 0, 0), - new Date(2010, 11, 31, 0, 0), - '/static/no-image.jpg', - 'png', - false, - true - ), - ]); + + _.zip(corpus.fields, fieldData).map(([result, expected]) => { + _.mapKeys(expected, key => { + expect(result[key]).toEqual(expected[key]); + }); + }); }); }); }); diff --git a/frontend/src/app/services/corpus.service.ts b/frontend/src/app/services/corpus.service.ts index 22cd53d73..fe4e4c47f 100644 --- a/frontend/src/app/services/corpus.service.ts +++ b/frontend/src/app/services/corpus.service.ts @@ -98,27 +98,7 @@ export class CorpusService { ); }; - private parseField = (data: any): CorpusField => ({ - description: data.description, - displayName: data.display_name || data.name, - displayType: data.display_type || data['es_mapping']?.type, - resultsOverview: data.results_overview, - csvCore: data.csv_core, - searchFieldCore: data.search_field_core, - visualizations: data.visualizations, - visualizationSort: data.visualization_sort, - multiFields: data['es_mapping']?.fields - ? Object.keys(data['es_mapping'].fields) - : undefined, - hidden: data.hidden, - sortable: data.sortable, - primarySort: data.primary_sort, - searchable: data.searchable, - downloadable: data.downloadable, - name: data.name, - searchFilter: data['search_filter'] || null, - mappingType: data.es_mapping?.type, - }); + private parseField = (data: any): CorpusField => new CorpusField(data); private parseDate(date: any): Date { // months are zero-based! diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index 9cc62d2d7..c9fbe1772 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -1,61 +1,104 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { BehaviorSubject } from 'rxjs'; import { findByName } from '../app/utils/utils'; -import { BooleanFilterData, Corpus, CorpusField, SearchFilter } from '../app/models'; +import { BooleanFilterOptions } from '../app/models/search-filter-options'; +import { Corpus, CorpusField } from '../app/models'; -const mockFilterData: BooleanFilterData = { +const mockFilterOptions: BooleanFilterOptions = { checked: false, - filterType: 'BooleanFilter', -}; - -export const mockFilter: SearchFilter = { - fieldName: 'great_field', + name: 'BooleanFilter', description: 'Use this filter to decide whether or not this field is great', - currentData: mockFilterData, - defaultData: mockFilterData, - useAsFilter: true, }; -export const mockField: CorpusField = { +export const mockField = new CorpusField({ name: 'great_field', description: 'A really wonderful field', - displayName: 'Greatest field', - displayType: 'keyword', - mappingType: 'keyword', + display_name: 'Greatest field', + display_type: 'keyword', + es_mapping: {type: 'keyword'}, hidden: false, sortable: false, - primarySort: false, + primary_sort: false, searchable: false, downloadable: false, - searchFilter: mockFilter -}; + search_filter: mockFilterOptions, + results_overview: true, + search_field_core: true, + csv_core: true, + visualizations: [], + visualization_sort: null, + indexed: true, + required: false, +}); -export const mockField2: CorpusField = { +export const mockField2 = new CorpusField({ name: 'speech', description: 'A content field', - displayName: 'Speechiness', - displayType: 'text', - mappingType: 'text', + display_name: 'Speechiness', + display_type: 'text', + es_mapping: {type: 'text'}, hidden: false, sortable: false, - primarySort: false, + primary_sort: false, searchable: true, downloadable: true, - searchFilter: null -}; + search_filter: null, + results_overview: true, + search_field_core: true, + csv_core: true, + visualizations: [], + visualization_sort: null, + indexed: true, + required: false, +}); -export const mockField3: CorpusField = { +export const mockField3 = new CorpusField({ name: 'ordering', description: 'A field which can be sorted on', - displayName: 'Sort me', - displayType: 'integer', - mappingType: 'keyword', + display_name: 'Sort me', + display_type: 'integer', + es_mapping: {type: 'keyword'}, hidden: false, sortable: true, - primarySort: false, + primary_sort: false, searchable: false, downloadable: true, - searchFilter: null -}; + results_overview: true, + search_filter: null, + search_field_core: false, + csv_core: true, + visualizations: [], + visualization_sort: null, + indexed: true, + required: false, +}); + +export const mockFieldDate = new CorpusField({ + name: 'date', + display_name: 'Date', + description: '', + display_type: 'date', + hidden: false, + sortable: true, + primary_sort: false, + searchable: false, + downloadable: true, + search_filter: { + name: 'DateFilter', + lower: '1800-01-01', + upper: '1899-12-31', + description: '' + }, + es_mapping: {type: 'date'}, + results_overview: true, + search_field_core: false, + csv_core: true, + visualizations: [], + visualization_sort: null, + indexed: true, + required: false, +}); + export const mockCorpus: Corpus = { name: 'test1', From ccfc2e0d5ba181fe59821b5d0391e1f330c8687a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 17:54:46 +0100 Subject: [PATCH 074/262] draft query model --- frontend/src/app/models/query.spec.ts | 70 +++++++++++ frontend/src/app/models/query.ts | 106 +++++++++++++++-- frontend/src/app/models/search-filter.ts | 14 ++- frontend/src/app/models/sort-event.ts | 3 + frontend/src/app/services/param.service.ts | 52 +-------- frontend/src/app/utils/params.ts | 128 ++++----------------- 6 files changed, 199 insertions(+), 174 deletions(-) create mode 100644 frontend/src/app/models/query.spec.ts diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts new file mode 100644 index 000000000..d84dc26a9 --- /dev/null +++ b/frontend/src/app/models/query.spec.ts @@ -0,0 +1,70 @@ +import { Corpus, CorpusField } from './corpus'; +import { QueryModel } from './query'; + +const corpus: Corpus = { + name: 'mock-corpus', + title: 'Mock Corpus', + serverName: 'default', + description: '', + index: 'mock-corpus', + minDate: new Date('1800-01-01'), + maxDate: new Date('1900-01-01'), + image: '', + scan_image_type: null, + allow_image_download: true, + word_models_present: false, + fields: [ + new CorpusField({ + name: 'content', + display_name: 'Content', + display_type: 'text_content', + description: '', + hidden: false, + sortable: false, + searchable: true, + downloadable: true, + primary_sort: false, + search_filter: null, + es_mapping: { type: 'text'}, + search_field_core: true, + visualizations: [], + visualization_sort: null, + results_overview: true, + csv_core: true, + indexed: true, + required: false, + }), + new CorpusField({ + name: 'date', + display_name: 'Date', + display_type: 'date', + description: '', + hidden: false, + sortable: true, + searchable: false, + downloadable: true, + primary_sort: false, + search_filter: { + name: 'DateFilter', + lower: '1800-01-01', + upper: '1900-01-01', + description: '', + }, + es_mapping: { type: 'date'}, + search_field_core: true, + visualizations: [], + visualization_sort: null, + results_overview: true, + csv_core: true, + indexed: true, + required: false, + }), + ], +}; + +describe('QueryModel', () => { + it('should create', () => { + const query = new QueryModel(corpus); + expect(query).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 64d570262..52cbc228a 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,6 +1,11 @@ -import {SearchFilter } from '../models/index'; +import { ParamMap } from '@angular/router'; +import * as _ from 'lodash'; +import { Subject } from 'rxjs'; +import { Corpus, CorpusField, SortBy, SortDirection } from '../models/index'; import { EsQuery } from '../services'; -import { SearchFilterData } from './search-filter-old'; +import { highlightFromParams, queryFromParams, searchFieldsFromParams, sortSettingsFromParams, + sortSettingsToParams } from '../utils/params'; +import { SearchFilter } from './search-filter'; /** This is the query object as it is saved in the database.*/ export class QueryDb { @@ -55,18 +60,95 @@ export class QueryDb { public total_results: number; } -/** This is the client's representation of the query by the user, shared between components */ -export interface QueryModel { - queryText: string; - fields?: string[]; - filters?: SearchFilter[]; - sortBy?: string; - sortAscending?: boolean; - highlight?: number; -} - /** These are the from / size parameters emitted by the pagination component */ export interface SearchParameters { from: number; size: number; } + +export class QueryModel { + corpus: Corpus; + queryText: string; + searchFields: CorpusField[]; + filters: SearchFilter[] = []; + sortBy: SortBy = 'default'; + sortDirection: SortDirection; + highlightSize: number; + + update = new Subject(); + + constructor(corpus: Corpus) { + this.corpus = corpus; + } + + setQueryText(text?: string) { + this.queryText = text; + this.update.next(); + } + + addFilter(filter: SearchFilter) { + this.filters.push(filter); + filter.data.subscribe(data => { + this.update.next(); + }); + } + + setFromParams(params: ParamMap) { + this.queryText = queryFromParams(params); + this.searchFields = searchFieldsFromParams(params, this.corpus); + [this.sortBy, this.sortDirection] = sortSettingsFromParams(params, this.corpus.fields); + this.highlightSize = highlightFromParams(params); + this.update.next(); + } + + /**sortFromParams + * reset values to a blank query for the corpus + */ + reset(): void { + this.queryText = undefined; + this.searchFields = undefined; + this.filters = []; + this.sortBy = 'default'; + this.sortDirection = undefined; + this.update.next(); + } + + /** + * make a clone of the current query. + * optionally include querytext or a filter for the new query. + */ + clone(queryText?: string, addFilter?: SearchFilter) { + const newQuery = _.clone(this); // or cloneDeep? + if (queryText !== undefined) { + newQuery.setQueryText(queryText); + } + if (addFilter) { + newQuery.addFilter(addFilter); + } + return newQuery; + } + + toRouteParam(): {[param: string]: any} { + const queryTextParams = { query: this.queryText } || {}; + const searchFieldsParams = { fields: + this.searchFields ? this.searchFields.map(f => f.name).join(',') : null + }; + const filterParams = this.filters.map(f => f.toRouteParam()); + const sortParams = sortSettingsToParams(this.sortBy, this.sortDirection); + const highlightParams = this.highlightSize ? { highlight: this.highlightSize } : {}; + + return { + ...queryTextParams, + ...searchFieldsParams, + ...filterParams, + ...sortParams, + ...highlightParams, + }; + } + + // toEsQuery(): EsQuery { + // return { + // query: {} + // }; + // } +} diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 2eea86456..ba233dc69 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -5,7 +5,7 @@ import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, import { BooleanFilterOptions, DateFilterOptions, FilterOptions, MultipleChoiceFilterOptions, RangeFilterOptions } from './search-filter-options'; -abstract class SearchFilter { +abstract class AbstractSearchFilter { corpusField: CorpusField; defaultData: FilterData; data: BehaviorSubject; @@ -64,7 +64,7 @@ interface DateFilterData { max: Date; } -export class DateFilter extends SearchFilter { +export class DateFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: DateFilterOptions) { return { min: this.parseDate(filterOptions.min), @@ -114,7 +114,7 @@ export class DateFilter extends SearchFilter { } } -export class BooleanFilter extends SearchFilter { +export class BooleanFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: BooleanFilterOptions) { return false; @@ -143,7 +143,7 @@ export class BooleanFilter extends SearchFilter { type MultipleChoiceFilterData = string[]; -export class MultipleChoiceFilter extends SearchFilter { +export class MultipleChoiceFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: MultipleChoiceFilterOptions): MultipleChoiceFilterData { return []; } @@ -174,7 +174,7 @@ interface RangeFilterData { max: number; } -export class RangeFilter extends SearchFilter { +export class RangeFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: RangeFilterOptions): RangeFilterData { return { min: filterOptions.lower, @@ -210,7 +210,7 @@ export class RangeFilter extends SearchFilter { } } -export class AdHocFilter extends SearchFilter { +export class AdHocFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: FilterOptions) {} dataFromValue(value: any) { @@ -244,3 +244,5 @@ const parseMinMax = (value: string[]): [string, string] => { return [value[0], value[1]]; } }; + +export type SearchFilter = DateFilter | MultipleChoiceFilter | RangeFilter | BooleanFilter | AdHocFilter; diff --git a/frontend/src/app/models/sort-event.ts b/frontend/src/app/models/sort-event.ts index af3a5be7c..fd1daba5b 100644 --- a/frontend/src/app/models/sort-event.ts +++ b/frontend/src/app/models/sort-event.ts @@ -4,3 +4,6 @@ export interface SortEvent { ascending: boolean; field: CorpusField | undefined; } + +export type SortBy = CorpusField | 'relevance' | 'default'; +export type SortDirection = 'asc'|'desc'; diff --git a/frontend/src/app/services/param.service.ts b/frontend/src/app/services/param.service.ts index 259df597d..395d57d3a 100644 --- a/frontend/src/app/services/param.service.ts +++ b/frontend/src/app/services/param.service.ts @@ -3,64 +3,14 @@ import * as _ from 'lodash'; import { Injectable } from '@angular/core'; import { ParamMap } from '@angular/router'; -import { CorpusField, QueryModel } from '../models'; +import { Corpus, CorpusField, FoundDocument, QueryModel } from '../models'; import { SearchService } from './search.service'; -import { - filtersFromParams, highlightFromParams, paramForFieldName, queryFromParams, searchFieldsFromParams, - searchFilterDataToParam, sortSettingsFromParams -} from '../utils/params'; @Injectable() export class ParamService { constructor(private searchService: SearchService) { } - public queryModelFromParams(params: ParamMap, corpusFields: CorpusField[]) { - // copy fields so the state in components is isolated - const fields = _.cloneDeep(corpusFields); - const activeFilters = filtersFromParams(params, fields); - const highlight = highlightFromParams(params); - const query = queryFromParams(params); - const queryFields = searchFieldsFromParams(params); - const sortSettings = sortSettingsFromParams(params, fields); - return this.searchService.createQueryModel( - query, queryFields, activeFilters, sortSettings.field, sortSettings.ascending, highlight); - } - public queryModelToRoute(queryModel: QueryModel, usingDefaultSortField = false, nullableParams = []): any { - const route = { - query: queryModel.queryText || '' - }; - - if (queryModel.fields) { - route['fields'] = queryModel.fields.join(','); - } else { - route['fields'] = null; - } - - for (const filter of queryModel.filters.map(data => ({ - param: paramForFieldName(data.fieldName), - value: searchFilterDataToParam(data) - }))) { - route[filter.param] = filter.value; - } - - if (!usingDefaultSortField && queryModel.sortBy) { - route['sort'] = `${queryModel.sortBy},${queryModel.sortAscending ? 'asc' : 'desc'}`; - } else { - route['sort'] = null; - } - - if (queryModel.highlight) { - route['highlight'] = `${queryModel.highlight}`; - } else { - route['highlight'] = null; - } - - if (nullableParams.length) { - nullableParams.forEach( param => route[param] = null); - } - return route; - } } diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 25c2a2ac1..b86f0df81 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -1,7 +1,6 @@ import { ParamMap } from '@angular/router'; +import { Corpus, CorpusField, SortBy, SortDirection } from '../models'; import * as _ from 'lodash'; -import { contextFilterFromField, CorpusField, SearchFilter, SearchFilterData, searchFilterDataFromSettings } from '../models'; -import { findByName } from './utils'; /** omit keys that mapp to null */ export const omitNullParameters = (params: {[key: string]: any}): {[key: string]: any} => { @@ -12,10 +11,10 @@ export const omitNullParameters = (params: {[key: string]: any}): {[key: string] export const queryFromParams = (params: ParamMap): string => params.get('query'); -export const searchFieldsFromParams = (params: ParamMap): string[] | null => { +export const searchFieldsFromParams = (params: ParamMap, corpus: Corpus): CorpusField[] => { if (params.has('fields')) { - const selectedSearchFields = params.get('fields').split(','); - return selectedSearchFields; + const fieldNames = params.get('fields').split(','); + return corpus.fields.filter(field => fieldNames.includes(field.name)); } }; @@ -24,115 +23,34 @@ export const highlightFromParams = (params: ParamMap): number => // sort -export const sortSettingsToParams = (sortBy: CorpusField, direction: string): {sort: string} => { - const fieldName = sortBy !== undefined ? sortBy.name : 'relevance'; - return {sort:`${fieldName},${direction}`}; +export const sortSettingsToParams = (sortBy: SortBy, direction: SortDirection): {sort?: string} => { + let sortByName: string; + if (sortBy === 'default') { + return {}; + } else if (sortBy === 'relevance') { + sortByName = sortBy; + } else { + sortByName = sortBy.name; + } + return { sort: `${sortByName},${direction}` }; }; -export const sortSettingsFromParams = (params: ParamMap, corpusFields: CorpusField[]): {field: CorpusField; ascending: boolean} => { - let sortField: CorpusField; +export const sortSettingsFromParams = (params: ParamMap, corpusFields: CorpusField[]): [SortBy, SortDirection] => { + let sortBy: SortBy; let sortAscending = true; if (params.has('sort')) { const [sortParam, ascParam] = params.get('sort').split(','); sortAscending = ascParam === 'asc'; if ( sortParam === 'relevance' ) { - return { - field: undefined, - ascending: sortAscending - }; + return [sortParam, sortAscending ? 'asc' : 'desc']; } - sortField = findByName(corpusFields, sortParam); + sortBy = corpusFields.find(field => field.name === sortParam); } else { - sortField = corpusFields.find(field => field.primarySort); - } - return { - field: sortField, - ascending: sortAscending - }; -}; - - -interface SearchFilterSettings { - [fieldName: string]: SearchFilterData; -} - -/** - * Set the filter data from the query parameters and return whether any filters were actually set. - */ -export const filtersFromParams = (params: ParamMap, corpusFields: CorpusField[]): SearchFilter[] => { - const filterSettings = filterSettingsFromParams(params, corpusFields); - return applyFilterSettings(filterSettings, corpusFields); -}; - -const filterSettingsFromParams = (params: ParamMap, corpusFields: CorpusField[]): SearchFilterSettings => { - const settings = {}; - corpusFields.forEach(field => { - const param = paramForFieldName(field.name); - if (params.has(param)) { - let filterSettings = params.get(param).split(','); - if (filterSettings[0] === '') { - filterSettings = []; - } - const filterType = field.searchFilter ? field.searchFilter.currentData.filterType : undefined; - const data = searchFilterDataFromSettings(filterType, filterSettings, field); - settings[field.name] = data; - } - }); - - return settings; -}; - -const applyFilterSettings = (filterSettings: SearchFilterSettings, corpusFields: CorpusField[]) => { - corpusFields.forEach(field => { - if (_.has(filterSettings, field.name)) { - const searchFilter = field.searchFilter || contextFilterFromField(field); - const data = filterSettings[field.name]; - searchFilter.currentData = data; - searchFilter.useAsFilter = true; - field.searchFilter = searchFilter; - } else { - if (field.searchFilter) { - field.searchFilter.useAsFilter = false; - if (field.searchFilter.adHoc) { - field.searchFilter = null; - } - } - } - }); - - return corpusFields.filter( field => field.searchFilter && field.searchFilter.useAsFilter ).map( field => field.searchFilter ); -}; - -/*** - * Convert field name to string - */ -export const paramForFieldName = (fieldName: string) => - `${fieldName}`; - - -// --- set params from filters --- // - -export const searchFiltersToParams = (fields: CorpusField[]) => { - const params = {}; - fields.forEach( field => { - const paramName = paramForFieldName(field.name); - const value = field.searchFilter.useAsFilter? searchFilterDataToParam(field.searchFilter) : null; - params[paramName] = value; - }); - - return params; -}; - -export const searchFilterDataToParam = (filter: SearchFilter): string => { - switch (filter.currentData.filterType) { - case 'BooleanFilter': - return `${filter.currentData.checked}`; - case 'MultipleChoiceFilter': - return filter.currentData.selected.join(','); - case 'RangeFilter': - return `${filter.currentData.min}:${filter.currentData.max}`; - case 'DateFilter': - return `${filter.currentData.min}:${filter.currentData.max}`; + sortBy = 'default'; } + return [ + sortBy, + sortAscending ? 'asc' : 'desc' + ]; }; From 38d41d6d7c93c46475ce7e227b8306e841ef6be6 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 18:32:36 +0100 Subject: [PATCH 075/262] make sort utils file --- frontend/src/app/utils/sort.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/utils/sort.ts b/frontend/src/app/utils/sort.ts index e4150ac1a..b817b8da5 100644 --- a/frontend/src/app/utils/sort.ts +++ b/frontend/src/app/utils/sort.ts @@ -1,2 +1,7 @@ +import { Corpus, SortBy } from '../models'; + +export const sortByDefault = (corpus: Corpus): SortBy => + corpus.fields.find(field => field.primarySort) || 'relevance'; + export const sortDirectionFromBoolean = (sortAscending: boolean): 'asc'|'desc' => sortAscending ? 'asc' : 'desc'; From ce7149326d7bb4c4173365fa31261ca7ae57928a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 9 Mar 2023 18:52:37 +0100 Subject: [PATCH 076/262] fill in querymodel.toEsQuery() --- frontend/src/app/models/query.spec.ts | 63 +++++++----------------- frontend/src/app/models/query.ts | 28 +++++++++-- frontend/src/app/utils/es-query.ts | 19 ++++--- frontend/src/app/utils/sort.ts | 5 +- frontend/src/mock-data/corpus.ts | 4 ++ frontend/src/mock-data/elastic-search.ts | 7 --- frontend/src/mock-data/search.ts | 28 ++++++----- 7 files changed, 73 insertions(+), 81 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index d84dc26a9..8954c690d 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -1,4 +1,5 @@ -import { Corpus, CorpusField } from './corpus'; +import { mockField2, mockFieldDate } from '../../mock-data/corpus'; +import { Corpus, } from './corpus'; import { QueryModel } from './query'; const corpus: Corpus = { @@ -14,51 +15,8 @@ const corpus: Corpus = { allow_image_download: true, word_models_present: false, fields: [ - new CorpusField({ - name: 'content', - display_name: 'Content', - display_type: 'text_content', - description: '', - hidden: false, - sortable: false, - searchable: true, - downloadable: true, - primary_sort: false, - search_filter: null, - es_mapping: { type: 'text'}, - search_field_core: true, - visualizations: [], - visualization_sort: null, - results_overview: true, - csv_core: true, - indexed: true, - required: false, - }), - new CorpusField({ - name: 'date', - display_name: 'Date', - display_type: 'date', - description: '', - hidden: false, - sortable: true, - searchable: false, - downloadable: true, - primary_sort: false, - search_filter: { - name: 'DateFilter', - lower: '1800-01-01', - upper: '1900-01-01', - description: '', - }, - es_mapping: { type: 'date'}, - search_field_core: true, - visualizations: [], - visualization_sort: null, - results_overview: true, - csv_core: true, - indexed: true, - required: false, - }), + mockField2, + mockFieldDate, ], }; @@ -67,4 +25,17 @@ describe('QueryModel', () => { const query = new QueryModel(corpus); expect(query).toBeTruthy(); }); + + it('should convert to an elasticsearch query', () => { + const query = new QueryModel(corpus); + + expect(query.toEsQuery()).toEqual({ + query: { + match_all: {} + } + }); + + query.setQueryText('test'); + + }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 52cbc228a..ef68408e6 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -3,8 +3,10 @@ import * as _ from 'lodash'; import { Subject } from 'rxjs'; import { Corpus, CorpusField, SortBy, SortDirection } from '../models/index'; import { EsQuery } from '../services'; +import { combineSearchClauseAndFilters, makeEsSearchClause, makeHighlightSpecification, makeSortSpecification } from '../utils/es-query'; import { highlightFromParams, queryFromParams, searchFieldsFromParams, sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; +import { sortByDefault } from '../utils/sort'; import { SearchFilter } from './search-filter'; /** This is the query object as it is saved in the database.*/ @@ -81,6 +83,15 @@ export class QueryModel { this.corpus = corpus; } + /** sort direction to be used in searching: replaces 'default' with the default value */ + private get actualSortBy(): CorpusField|'relevance' { + if (this.sortBy !== 'default') { + return this.sortBy; + } else { + return sortByDefault(this.corpus); + } + } + setQueryText(text?: string) { this.queryText = text; this.update.next(); @@ -146,9 +157,16 @@ export class QueryModel { }; } - // toEsQuery(): EsQuery { - // return { - // query: {} - // }; - // } + toEsQuery(): EsQuery { + const searchClause = makeEsSearchClause(this.queryText, this.searchFields); + const filters = this.filters.map(filter => filter.toEsFilter()); + const query = combineSearchClauseAndFilters(searchClause, filters); + + const sort = makeSortSpecification(this.actualSortBy, this.sortDirection); + const highlight = makeHighlightSpecification(this.corpus, this.queryText, this.highlightSize); + + return { + ...query, ...sort, ...highlight + }; + } } diff --git a/frontend/src/app/utils/es-query.ts b/frontend/src/app/utils/es-query.ts index c6de8a5c2..d50ebff11 100644 --- a/frontend/src/app/utils/es-query.ts +++ b/frontend/src/app/utils/es-query.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as _ from 'lodash'; -import { BooleanQuery, Corpus, CorpusField, EsFilter, EsSearchClause, MatchAll, SimpleQueryString } from '../models'; -import { sortDirectionFromBoolean } from './sort'; - +import { BooleanQuery, Corpus, CorpusField, EsFilter, EsSearchClause, MatchAll, SimpleQueryString, SortDirection } from '../models'; +import { EsQuery } from '../services'; // conversion from query model -> elasticsearch query language @@ -41,13 +40,17 @@ export const makeBooleanQuery = (query: EsSearchClause, filters: EsFilter[]): Bo } }); +export const combineSearchClauseAndFilters = (searchClause: EsSearchClause, filters?: EsFilter[]): EsQuery => { + const query = (filters && filters.length) ? makeBooleanQuery(searchClause, filters) : searchClause; + return { query }; +}; -export const makeSortSpecification = (sortBy: string, sortAscending: boolean) => { - if (!sortBy) { +export const makeSortSpecification = (sortBy: CorpusField|'relevance', sortDirection: SortDirection) => { + if (sortBy === 'relevance') { return {}; } else { const sortByField = { - [sortBy]: sortDirectionFromBoolean(sortAscending) + [sortBy.name]: sortDirection }; return { sort: [sortByField] @@ -55,11 +58,11 @@ export const makeSortSpecification = (sortBy: string, sortAscending: boolean) => } }; -export const makeHighlightSpecification = (corpusFields: CorpusField[], queryText?: string, highlightSize?: number) => { +export const makeHighlightSpecification = (corpus: Corpus, queryText?: string, highlightSize?: number) => { if (!queryText || !highlightSize) { return {}; } - const highlightFields = corpusFields.filter(field => field.searchable); + const highlightFields = corpus.fields.filter(field => field.searchable); return { highlight: { fragment_size: highlightSize, diff --git a/frontend/src/app/utils/sort.ts b/frontend/src/app/utils/sort.ts index b817b8da5..e6133e091 100644 --- a/frontend/src/app/utils/sort.ts +++ b/frontend/src/app/utils/sort.ts @@ -1,6 +1,7 @@ -import { Corpus, SortBy } from '../models'; +import { Corpus, CorpusField, SortBy } from '../models'; -export const sortByDefault = (corpus: Corpus): SortBy => +/** get the default sortBy for a corpus */ +export const sortByDefault = (corpus: Corpus): CorpusField|'relevance' => corpus.fields.find(field => field.primarySort) || 'relevance'; export const sortDirectionFromBoolean = (sortAscending: boolean): 'asc'|'desc' => diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index c9fbe1772..729224046 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -10,6 +10,7 @@ const mockFilterOptions: BooleanFilterOptions = { description: 'Use this filter to decide whether or not this field is great', }; +/** a keyword field with a boolean filter */ export const mockField = new CorpusField({ name: 'great_field', description: 'A really wonderful field', @@ -31,6 +32,7 @@ export const mockField = new CorpusField({ required: false, }); +/** a text content field */ export const mockField2 = new CorpusField({ name: 'speech', description: 'A content field', @@ -52,6 +54,7 @@ export const mockField2 = new CorpusField({ required: false, }); +/** a keyword field with sorting option */ export const mockField3 = new CorpusField({ name: 'ordering', description: 'A field which can be sorted on', @@ -73,6 +76,7 @@ export const mockField3 = new CorpusField({ required: false, }); +/** a date field */ export const mockFieldDate = new CorpusField({ name: 'date', display_name: 'Date', diff --git a/frontend/src/mock-data/elastic-search.ts b/frontend/src/mock-data/elastic-search.ts index 4a45c2061..942f2fa3e 100644 --- a/frontend/src/mock-data/elastic-search.ts +++ b/frontend/src/mock-data/elastic-search.ts @@ -8,13 +8,6 @@ export class ElasticSearchServiceMock { public clearScroll() { } - esQueryToQueryModel(query: EsQuery, corpus: Corpus): QueryModel { - return { - queryText: '', - filters: [] - }; - } - getDocumentById(): Promise { return Promise.resolve({ id: '0', diff --git a/frontend/src/mock-data/search.ts b/frontend/src/mock-data/search.ts index 8e88c9cff..d2cc8282a 100644 --- a/frontend/src/mock-data/search.ts +++ b/frontend/src/mock-data/search.ts @@ -1,4 +1,5 @@ -import { AggregateQueryFeedback, Corpus, CorpusField, QueryModel, SearchFilter, SearchFilterData } from '../app/models/index'; +import { SearchFilter } from '../app/models/search-filter'; +import { AggregateQueryFeedback, Corpus, CorpusField, QueryModel } from '../app/models/index'; export class SearchServiceMock { public async aggregateSearch(corpus: Corpus, queryModel: QueryModel, aggregator: string): Promise { @@ -21,21 +22,22 @@ export class SearchServiceMock { public async getRelatedWords() {} createQueryModel( - queryText: string = '', fields: string[] | null = null, filters: SearchFilter[] = [], + corpus: Corpus, + queryText: string = '', fields: CorpusField[] | null = null, filters: SearchFilter[] = [], sortField: CorpusField = null, sortAscending = false, highlight: number = null ): QueryModel { - const model: QueryModel = { - queryText, - filters, - sortBy: sortField ? sortField.name : undefined, - sortAscending - }; - if (fields) { - model.fields = fields; - } - if (highlight) { - model.highlight = highlight; + const model = new QueryModel(corpus); + model.setQueryText(queryText); + model.searchFields = fields; + filters.forEach(model.addFilter); + + if (sortField) { + model.sortBy = sortField; + model.sortDirection = sortAscending ? 'asc' : 'desc'; } + + model.highlightSize = highlight; + return model; } } From 5f8ce7e47e62b3e83ee1593cb71d042d197077e3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 10 Mar 2023 12:32:43 +0100 Subject: [PATCH 077/262] implement esQuerytoQueryModel with new models --- frontend/src/app/models/search-filter.ts | 51 +++++++++++++++++++----- frontend/src/app/utils/es-query.ts | 39 +++++++++++++++++- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index ba233dc69..d28534298 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import * as moment from 'moment'; import { BehaviorSubject } from 'rxjs'; import { CorpusField } from './corpus'; @@ -5,14 +6,14 @@ import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, import { BooleanFilterOptions, DateFilterOptions, FilterOptions, MultipleChoiceFilterOptions, RangeFilterOptions } from './search-filter-options'; -abstract class AbstractSearchFilter { +abstract class AbstractSearchFilter { corpusField: CorpusField; defaultData: FilterData; data: BehaviorSubject; constructor(corpusField: CorpusField) { this.corpusField = corpusField; - this.defaultData = this.makeDefaultData(corpusField.searchFilter.defaultData); + this.defaultData = this.makeDefaultData(corpusField.filterOptions); this.data = new BehaviorSubject(this.defaultData); } @@ -56,7 +57,9 @@ abstract class AbstractSearchFilter { /** * export as filter specification in elasticsearch query language */ - abstract toEsFilter(): EsFilter; + abstract toEsFilter(): EsFilterType; + + abstract dataFromEsFilter(esFilter: EsFilterType): FilterData; } interface DateFilterData { @@ -64,11 +67,11 @@ interface DateFilterData { max: Date; } -export class DateFilter extends AbstractSearchFilter { +export class DateFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: DateFilterOptions) { return { - min: this.parseDate(filterOptions.min), - max: this.parseDate(filterOptions.max) + min: this.parseDate(filterOptions.lower), + max: this.parseDate(filterOptions.upper) }; } @@ -105,6 +108,13 @@ export class DateFilter extends AbstractSearchFilter { }; } + dataFromEsFilter(esFilter: EsDateFilter): DateFilterData { + const data = _.first(_.values(esFilter.range)); + const min = this.parseDate(data.gte); + const max = this.parseDate(data.lte); + return { min, max }; + } + private formatDate(date: Date): string { return moment(date).format('YYYY-MM-DD'); } @@ -114,7 +124,7 @@ export class DateFilter extends AbstractSearchFilter { } } -export class BooleanFilter extends AbstractSearchFilter { +export class BooleanFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: BooleanFilterOptions) { return false; @@ -139,11 +149,16 @@ export class BooleanFilter extends AbstractSearchFilter { } }; } + + dataFromEsFilter(esFilter: EsBooleanFilter): boolean { + const data = _.first(_.values(esFilter.term)); + return data; + } } type MultipleChoiceFilterData = string[]; -export class MultipleChoiceFilter extends AbstractSearchFilter { +export class MultipleChoiceFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: MultipleChoiceFilterOptions): MultipleChoiceFilterData { return []; } @@ -167,6 +182,10 @@ export class MultipleChoiceFilter extends AbstractSearchFilter { +export class RangeFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: RangeFilterOptions): RangeFilterData { return { min: filterOptions.lower, @@ -208,9 +227,16 @@ export class RangeFilter extends AbstractSearchFilter { } }; } + + dataFromEsFilter(esFilter: EsRangeFilter): RangeFilterData { + const data = _.first(_.values(esFilter.range)); + const min = data.gte; + const max = data.lte; + return { min, max }; + } } -export class AdHocFilter extends AbstractSearchFilter { +export class AdHocFilter extends AbstractSearchFilter { makeDefaultData(filterOptions: FilterOptions) {} dataFromValue(value: any) { @@ -232,6 +258,11 @@ export class AdHocFilter extends AbstractSearchFilter { } }; } + + dataFromEsFilter(esFilter: EsTermFilter): string { + const data = _.first(_.values(esFilter.term)); + return data; + } } const parseMinMax = (value: string[]): [string, string] => { diff --git a/frontend/src/app/utils/es-query.ts b/frontend/src/app/utils/es-query.ts index d50ebff11..e246fa66a 100644 --- a/frontend/src/app/utils/es-query.ts +++ b/frontend/src/app/utils/es-query.ts @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as _ from 'lodash'; -import { BooleanQuery, Corpus, CorpusField, EsFilter, EsSearchClause, MatchAll, SimpleQueryString, SortDirection } from '../models'; +import { BooleanQuery, Corpus, CorpusField, EsFilter, EsSearchClause, MatchAll, + QueryModel, + SimpleQueryString, SortDirection } from '../models'; import { EsQuery } from '../services'; +import { SearchFilter } from '../models/search-filter'; // conversion from query model -> elasticsearch query language @@ -76,3 +79,37 @@ export const makeHighlightSpecification = (corpus: Corpus, queryText?: string, h }; }; +// conversion from elasticsearch query language -> query model + +export const esQueryToQueryModel = (query: EsQuery, corpus: Corpus): QueryModel => { + const model = new QueryModel(corpus); + model.setQueryText(queryTextFromEsSearchClause(query.query)); + const filters = filtersFromEsQuery(query, corpus); + filters.forEach(filter => model.addFilter(filter)); + return model; +}; + +const queryTextFromEsSearchClause = (query: EsSearchClause | BooleanQuery | EsFilter): string => { + const clause = 'bool' in query ? query.bool.must : query; + + if ('simple_query_string' in clause) { + return clause.simple_query_string.query; + } +}; + +const filtersFromEsQuery = (query: EsQuery, corpus: Corpus): SearchFilter[] => { + if ('bool' in query.query) { + const filters = query.query.bool.filter; + return filters.map(filter => esFilterToSearchFilter(filter, corpus)); + } + return []; +}; + +const esFilterToSearchFilter = (esFilter: EsFilter, corpus: Corpus): SearchFilter => { + const filterType = _.first(_.keys(esFilter)) as 'term'|'terms'|'range'; + const fieldName = _.first(_.keys(esFilter[filterType])); + const field = corpus.fields.find(f => f.name === fieldName); + const filter = field.makeSearchFilter(); + filter.data.next(filter.dataFromEsFilter(esFilter as any)); // we know that the esFilter is of the correct type + return filter; +}; From 6080abff0bd7dcb539d57cf7ed2e7e773a9c823a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 11:39:01 +0100 Subject: [PATCH 078/262] remove obsolete functions from elasticsearch service --- .../services/elastic-search.service.spec.ts | 80 ----------- .../app/services/elastic-search.service.ts | 129 ++---------------- 2 files changed, 11 insertions(+), 198 deletions(-) diff --git a/frontend/src/app/services/elastic-search.service.spec.ts b/frontend/src/app/services/elastic-search.service.spec.ts index b442105c1..6978e5941 100644 --- a/frontend/src/app/services/elastic-search.service.spec.ts +++ b/frontend/src/app/services/elastic-search.service.spec.ts @@ -2,66 +2,6 @@ import { TestBed, inject } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ElasticSearchService } from './elastic-search.service'; import { Corpus, DateFilterData, QueryModel, SearchFilter } from '../models'; - - -const dateFilter: SearchFilter = { - fieldName: 'date', - description: '', - useAsFilter: true, - defaultData: { - filterType: 'DateFilter', - min: '1099-01-01', - max: '1300-12-31' - }, - currentData: { - filterType: 'DateFilter', - min: '1111-01-01', - max: '1299-12-31' - } -}; - -const mockCorpus: Corpus = { - serverName: '', - name: 'mock-corpus', - title: 'Mock Corpus', - description: '', - index: 'mock-corpus', - minDate: new Date('1800-01-01'), - maxDate: new Date('1900-01-01'), - image: 'image.jpeg', - scan_image_type: undefined, - allow_image_download: true, - word_models_present: false, - fields: [ - { - name: 'content', - displayName: 'Content', - description: '', - displayType: 'text_content', - hidden: false, - sortable: false, - primarySort: false, - searchable: true, - downloadable: true, - searchFilter: undefined, - mappingType: 'text', - }, - { - name: 'date', - displayName: 'Date', - description: '', - displayType: 'date', - hidden: false, - sortable: true, - primarySort: false, - searchable: false, - downloadable: true, - searchFilter: dateFilter, - mappingType: 'date' - } - ], -}; - describe('ElasticSearchService', () => { let service: ElasticSearchService; @@ -78,24 +18,4 @@ describe('ElasticSearchService', () => { it('should be created',() => { expect(service).toBeTruthy(); }); - - it('should convert between EsQuery and QueryModel types', () => { - - const querymodels: QueryModel[] = [ - { - queryText: 'test', - filters: [], - }, - { - queryText: 'test', - filters: [ dateFilter ] - } - ]; - - querymodels.forEach(queryModel => { - const esQuery = service.makeEsQuery(queryModel); - const restoredQueryModel = service.esQueryToQueryModel(esQuery, mockCorpus); - expect(restoredQueryModel).toEqual(queryModel); - }); - }); }); diff --git a/frontend/src/app/services/elastic-search.service.ts b/frontend/src/app/services/elastic-search.service.ts index b9a69f7a9..89f0fdd7a 100644 --- a/frontend/src/app/services/elastic-search.service.ts +++ b/frontend/src/app/services/elastic-search.service.ts @@ -1,16 +1,13 @@ /* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable @typescript-eslint/member-ordering */ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { FoundDocument, Corpus, CorpusField, QueryModel, SearchResults, - AggregateQueryFeedback, SearchFilter, SearchFilterData, searchFilterDataFromField, - EsFilter, EsDateFilter, EsRangeFilter, EsTermsFilter, EsBooleanFilter, - EsSearchClause, BooleanQuery, MatchAll } from '../models/index'; - - +import { + FoundDocument, Corpus, QueryModel, SearchResults, + AggregateQueryFeedback, EsSearchClause, BooleanQuery, EsFilter +} from '../models/index'; import * as _ from 'lodash'; -import { findByName } from '../utils/utils'; -import { makeBooleanQuery, makeEsSearchClause, makeHighlightSpecification, makeSortSpecification, } from '../utils/es-query'; @Injectable() @@ -22,40 +19,6 @@ export class ElasticSearchService { this.client = new Client(this.http); } - public makeEsQuery(queryModel: QueryModel, fields?: CorpusField[]): EsQuery | EsQuerySorted { - const clause: EsSearchClause = makeEsSearchClause(queryModel.queryText, fields); - - let query: EsQuery | EsQuerySorted; - if (queryModel.filters) { - query = { - query: makeBooleanQuery(clause, this.mapFilters(queryModel.filters)) - }; - } else { - query = { - query: clause - }; - } - - const sort = makeSortSpecification(queryModel.sortBy, queryModel.sortAscending); - _.merge(query, sort); - - const highlight = makeHighlightSpecification(fields, queryModel.queryText, queryModel.highlight); - _.merge(query, highlight); - - return query; - } - - public esQueryToQueryModel(query: EsQuery, corpus: Corpus): QueryModel { - const queryText = this.queryTextFromEsSearchClause(query.query); - const filters = this.filtersFromEsQuery(query, corpus); - - if (filters.length) { - return { queryText, filters }; - } else { - return { queryText, filters: [] }; - } - } - getDocumentById(id: string, corpus: Corpus): Promise { const query = { body: { @@ -78,47 +41,6 @@ export class ElasticSearchService { } } - private queryTextFromEsSearchClause(query: EsSearchClause | BooleanQuery | EsFilter): string { - const clause = 'bool' in query ? query.bool.must : query; - - if ('simple_query_string' in clause) { - return clause.simple_query_string.query; - } - } - - private filtersFromEsQuery(query: EsQuery, corpus: Corpus): SearchFilter[] { - if ('bool' in query.query) { - const filters = query.query.bool.filter; - return filters.map(filter => this.esFilterToSearchFilter(filter, corpus)); - } - return []; - } - - private esFilterToSearchFilter(filter: EsFilter, corpus: Corpus): SearchFilter { - let fieldName: string; - let value: any; - - if ('term' in filter) { // boolean filter - fieldName = _.keys(filter.term)[0]; - value = filter.term[fieldName]; - } else if ('terms' in filter) { // multiple choice filter - fieldName = _.keys(filter.terms)[0]; - value = filter.terms[fieldName]; - } else { // range or date filter - fieldName = _.keys(filter.range)[0]; - value = [filter.range[fieldName].gte.toString(), filter.range[fieldName].lte.toString()]; - } - const field: CorpusField = findByName(corpus.fields, fieldName); - const filterData = searchFilterDataFromField(field, value); - return { - fieldName: field.name, - description: field.searchFilter?.description || '', - useAsFilter: true, - currentData: filterData, - defaultData: field.searchFilter?.defaultData, - }; - } - /** * Construct the aggregator, based on kind of field * Date fields are aggregated in year intervals @@ -162,7 +84,7 @@ export class ElasticSearchService { aggregators.forEach(d => { aggregations[d.name] = this.makeAggregation(d.name, d.size, 1); }); - const esQuery = this.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); const aggregationModel = Object.assign({ aggs: aggregations }, esQuery); const result = await this.executeAggregate(corpusDefinition, aggregationModel); const aggregateData = {}; @@ -188,7 +110,7 @@ export class ElasticSearchService { } } }; - const esQuery = this.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); const aggregationModel = Object.assign({ aggs: agg }, esQuery); const result = await this.executeAggregate(corpusDefinition, aggregationModel); const aggregateData = {}; @@ -208,7 +130,8 @@ export class ElasticSearchService { queryModel: QueryModel, size?: number, ): Promise { - const esQuery = this.makeEsQuery(queryModel, corpusDefinition.fields); + const esQuery = queryModel.toEsQuery(); + // Perform the search const response = await this.execute(corpusDefinition, esQuery, size || this.resultsPerPage); return this.parseResponse(response); @@ -222,7 +145,7 @@ export class ElasticSearchService { corpusDefinition: Corpus, queryModel: QueryModel, from: number, size: number): Promise { - const esQuery = this.makeEsQuery(queryModel, corpusDefinition.fields); + const esQuery = queryModel.toEsQuery(); // Perform the search const response = await this.execute(corpusDefinition, esQuery, size || this.resultsPerPage, from); return this.parseResponse(response); @@ -247,7 +170,7 @@ export class ElasticSearchService { /** * return the id, relevance and field values of a given document */ - private hitToDocument(hit: SearchHit, maxScore: number): FoundDocument { + private hitToDocument(hit: SearchHit, maxScore: number) { return { id: hit._id, relevance: hit._score / maxScore, @@ -255,36 +178,6 @@ export class ElasticSearchService { highlight: hit.highlight, } as FoundDocument; } - - /** - * Convert filters from query model into elasticsearch form - */ - private mapFilters(filters: SearchFilter[]): EsFilter[] { - return filters.map(filter => { - switch (filter.currentData.filterType) { - case 'BooleanFilter': - return { term: { [filter.fieldName]: filter.currentData.checked } }; - case 'MultipleChoiceFilter': - return { - terms: { - [filter.fieldName]: _.map(filter.currentData.selected, f => decodeURIComponent(f)) - } - }; - case 'RangeFilter': - return { - range: { - [filter.fieldName]: { gte: filter.currentData.min, lte: filter.currentData.max } - } - }; - case 'DateFilter': - return { - range: { - [filter.fieldName]: { gte: filter.currentData.min, lte: filter.currentData.max, format: 'yyyy-MM-dd' } - } - }; - } - }); - } } interface Connection { From 2cffb9272c89a05cfd33deb6f80dbabe15d28b03 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 13:03:59 +0100 Subject: [PATCH 079/262] use querymodel.toesquery() --- frontend/src/app/services/download.service.ts | 6 +-- frontend/src/app/services/search.service.ts | 42 ++----------------- .../src/app/services/visualization.service.ts | 13 +++--- 3 files changed, 11 insertions(+), 50 deletions(-) diff --git a/frontend/src/app/services/download.service.ts b/frontend/src/app/services/download.service.ts index c313eabf7..27a8d0eaa 100644 --- a/frontend/src/app/services/download.service.ts +++ b/frontend/src/app/services/download.service.ts @@ -24,8 +24,7 @@ export class DownloadService { requestedResults: number, route: string, highlightFragmentSize: number, fileOptions: DownloadOptions ): Promise { - const esQuery = this.elasticSearchService.makeEsQuery( - queryModel, corpus.fields); // to create elastic search query + const esQuery = queryModel.toEsQuery(); // to create elastic search query const parameters = _.merge( { corpus: corpus.name, @@ -58,8 +57,7 @@ export class DownloadService { * @param queryModel QueryModel for which download is requested. * @param fields The fields to appear as columns in the csv. */ - const esQuery = this.elasticSearchService.makeEsQuery( - queryModel, corpus.fields); // to create elastic search query + const esQuery = queryModel.toEsQuery(); // to create elastic search query return this.apiService.downloadTask({corpus: corpus.name, es_query: esQuery, fields: fields.map( field => field.name ), route }) .then(result => result) .catch( error => { diff --git a/frontend/src/app/services/search.service.ts b/frontend/src/app/services/search.service.ts index 8a005a047..8c68c2ab8 100644 --- a/frontend/src/app/services/search.service.ts +++ b/frontend/src/app/services/search.service.ts @@ -4,14 +4,8 @@ import { ApiService } from './api.service'; import { ElasticSearchService } from './elastic-search.service'; import { QueryService } from './query.service'; import { - Corpus, - CorpusField, - QueryModel, - SearchFilter, - SearchResults, - AggregateQueryFeedback, - SearchFilterData, - QueryDb, + Corpus, QueryModel, SearchResults, + AggregateQueryFeedback, QueryDb } from '../models/index'; import { AuthService } from './auth.service'; @@ -46,42 +40,12 @@ export class SearchService { return results; } - /** - * Construct a dictionary representing an ES query. - * - * @param queryString Read as the `simple_query_string` DSL of standard ElasticSearch. - * @param fields Optional list of fields to restrict the queryString to. - * @param filters A list of dictionaries representing the ES DSL. - */ - public createQueryModel( - queryText: string = '', - fields: string[] | null = null, - filters: SearchFilter[] = [], - sortField: CorpusField = null, - sortAscending = false, - highlight: number = null - ): QueryModel { - const model: QueryModel = { - queryText, - filters, - sortBy: sortField ? sortField.name : undefined, - sortAscending, - }; - if (fields) { - model.fields = fields; - } - if (highlight) { - model.highlight = highlight; - } - return model; - } - public async search( queryModel: QueryModel, corpus: Corpus ): Promise { const user = await this.authService.getCurrentUserPromise(); - const esQuery = this.elasticSearchService.makeEsQuery(queryModel, corpus.fields); + const esQuery = queryModel.toEsQuery(); const query = new QueryDb(esQuery, corpus.name, user.id); query.started = new Date(Date.now()); const results = await this.elasticSearchService.search( diff --git a/frontend/src/app/services/visualization.service.ts b/frontend/src/app/services/visualization.service.ts index df042aeec..647028cd6 100644 --- a/frontend/src/app/services/visualization.service.ts +++ b/frontend/src/app/services/visualization.service.ts @@ -18,20 +18,19 @@ import { ElasticSearchService } from './elastic-search.service'; export class VisualizationService { constructor( - private apiService: ApiService, - private elasticSearchService: ElasticSearchService) { + private apiService: ApiService) { window['apiService'] = this.apiService; } public async getWordcloudData(fieldName: string, queryModel: QueryModel, corpus: string, size: number): Promise { - const esQuery = this.elasticSearchService.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); return this.apiService.wordcloud({es_query: esQuery, corpus, field: fieldName, size}); } public async getWordcloudTasks(fieldName: string, queryModel: QueryModel, corpus: string): Promise { - const esQuery = this.elasticSearchService.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); return this.apiService.wordcloudTasks({es_query: esQuery, corpus, field: fieldName}) .then(result =>result['task_ids']); } @@ -39,7 +38,7 @@ export class VisualizationService { public makeAggregateTermFrequencyParameters( corpus: Corpus, queryModel: QueryModel, fieldName: string, bins: {fieldValue: string|number; size: number}[], ): AggregateTermFrequencyParameters { - const esQuery = this.elasticSearchService.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); return { corpus_name: corpus.name, es_query: esQuery, @@ -59,7 +58,7 @@ export class VisualizationService { corpus: Corpus, queryModel: QueryModel, fieldName: string, bins: {size: number; start_date: Date; end_date?: Date}[], unit: TimeCategory, ): DateTermFrequencyParameters { - const esQuery = this.elasticSearchService.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); return { corpus_name: corpus.name, es_query: esQuery, @@ -82,7 +81,7 @@ export class VisualizationService { } getNgramTasks(queryModel: QueryModel, corpusName: string, field: string, params: NgramParameters): Promise { - const esQuery = this.elasticSearchService.makeEsQuery(queryModel); + const esQuery = queryModel.toEsQuery(); return this.apiService.ngramTasks({ es_query: esQuery, corpus_name: corpusName, From 7792d53bcad9ebbeaab771e15b0c7c47d644021e Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 13:14:17 +0100 Subject: [PATCH 080/262] start using querymodel functions in search --- .../app/search/search-results.component.ts | 5 +-- .../app/search/search-sorting.component.ts | 2 +- frontend/src/app/search/search.component.ts | 45 +++++++++++++------ 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/search/search-results.component.ts b/frontend/src/app/search/search-results.component.ts index 0e272f8a0..52cd8bea4 100644 --- a/frontend/src/app/search/search-results.component.ts +++ b/frontend/src/app/search/search-results.component.ts @@ -50,8 +50,6 @@ export class SearchResultsComponent implements OnChanges { public fromIndex = 0; - public queryText: string; - public imgSrc: Uint8Array; /** @@ -78,10 +76,9 @@ export class SearchResultsComponent implements OnChanges { ngOnChanges() { if (this.queryModel !== null) { - this.queryText = this.queryModel.queryText; this.fromIndex = 0; this.maximumDisplayed = this.user.downloadLimit ? this.user.downloadLimit : 10000; - this.search(); + this.queryModel.update.subscribe(() => this.search()); } } diff --git a/frontend/src/app/search/search-sorting.component.ts b/frontend/src/app/search/search-sorting.component.ts index cfd0954c2..1e41cdf4d 100644 --- a/frontend/src/app/search/search-sorting.component.ts +++ b/frontend/src/app/search/search-sorting.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { CorpusField } from '../models'; +import { CorpusField, QueryModel } from '../models'; import { ParamDirective } from '../param/param-directive'; import { sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; import { sortDirectionFromBoolean } from '../utils/sort'; diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index b220b4479..d8ecd23f0 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -3,8 +3,7 @@ import { Component, ElementRef, ViewChild, HostListener } from '@angular/core'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; import * as _ from 'lodash'; -import { Corpus, CorpusField, ResultOverview, QueryModel, User, contextFilterFromField, SearchFilterData, - SearchFilter, +import { Corpus, CorpusField, ResultOverview, QueryModel, User, SearchFilter, FoundDocument} from '../models/index'; import { CorpusService, DialogService, ParamService, SearchService } from '../services/index'; import { ParamDirective } from '../param/param-directive'; @@ -81,10 +80,7 @@ export class SearchComponent extends ParamDirective { setStateFromParams(params: ParamMap) { this.queryText = params.get('query'); - const queryModel = this.paramService.queryModelFromParams(params, this.corpus.fields); - if (!_.isEqual(this.queryModel, queryModel)) { - this.queryModel = queryModel; - } + this.queryModel.setFromParams(params); this.tabIndex = params.has('visualize') ? 1 : 0; this.showVisualization = params.has('visualize') ? true : false; } @@ -124,12 +120,7 @@ export class SearchComponent extends ParamDirective { } public search() { - this.setParams({ query: this.queryText }); - } - - public goToContext(document: FoundDocument) { - const params = makeContextParams(document, this.corpus); - this.setParams(params); + this.queryModel.setQueryText(this.queryText); } /** @@ -139,9 +130,35 @@ export class SearchComponent extends ParamDirective { private setCorpus(corpus: Corpus) { if (!this.corpus || this.corpus.name !== corpus.name) { this.corpus = corpus; - this.filterFields = this.corpus.fields.filter(field => field.searchFilter); - this.queryModel = {queryText: ''}; + this.setQueryModel(); } } + private setQueryModel() { + this.queryModel = new QueryModel(this.corpus); + this.queryModel.update.subscribe(() => { + this.setParams(this.queryModel.toRouteParam()); + }); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + public goToContext(contextValues: any) { + const contextSpec = this.corpus.documentContext; + + const queryModel = new QueryModel(this.corpus); + + const contextFields = contextSpec.contextFields + .filter(field => ! this.filterFields.find(f => f.name === field.name)); + + contextFields.forEach(field => { + const filter = field.makeSearchFilter(); + filter.setToValue(contextValues[field.name]); + queryModel.addFilter(filter); + }); + + queryModel.sortBy = contextSpec.sortField; + queryModel.sortDirection = contextSpec.sortDirection; + + this.setParams(queryModel.toRouteParam()); + } } From 9d778be4c48308864e37b771e4ffc6946df94522 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 10 Mar 2023 11:43:54 +0100 Subject: [PATCH 081/262] add potentialfilter class --- .../src/app/models/filter-management.spec.ts | 24 ++++++++++++ frontend/src/app/models/filter-management.ts | 39 +++++++++++++++++++ frontend/src/app/models/index.ts | 4 +- frontend/src/app/models/query.ts | 12 ++++++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/models/filter-management.spec.ts create mode 100644 frontend/src/app/models/filter-management.ts diff --git a/frontend/src/app/models/filter-management.spec.ts b/frontend/src/app/models/filter-management.spec.ts new file mode 100644 index 000000000..38064b6a5 --- /dev/null +++ b/frontend/src/app/models/filter-management.spec.ts @@ -0,0 +1,24 @@ +import { mockCorpus, mockField } from '../../mock-data/corpus'; +import { PotentialFilter } from './filter-management'; +import { QueryModel } from './query'; + +describe('PotentialFilter', () => { + it('should create', () => { + const field = mockField; + const query = new QueryModel(mockCorpus); + const potentialFilter = new PotentialFilter(field, query); + expect(potentialFilter).toBeTruthy(); + }); + + it('should toggle', () => { + const field = mockField; + const query = new QueryModel(mockCorpus); + const potentialFilter = new PotentialFilter(field, query); + + expect(query.filters.length).toBe(0); + potentialFilter.toggle(); + expect(query.filters.length).toBe(1); + potentialFilter.toggle(); + expect(query.filters.length).toBe(0); + }); +}); diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts new file mode 100644 index 000000000..6533684e1 --- /dev/null +++ b/frontend/src/app/models/filter-management.ts @@ -0,0 +1,39 @@ +import { CorpusField } from './corpus'; +import { QueryModel } from './query'; +import { SearchFilter } from './search-filter'; + +export class PotentialFilter { + filter: SearchFilter; + useAsFilter: boolean; + showReset?: boolean; + grayedOut?: boolean; + adHoc?: boolean; + + constructor(public corpusField: CorpusField, public queryModel: QueryModel) { + this.filter = corpusField.makeSearchFilter(); + this.useAsFilter = false; + if (!corpusField.filterOptions) { + this.adHoc = true; + } + } + + toggle() { + this.useAsFilter = !this.useAsFilter; + if (this.useAsFilter) { + this.queryModel.addFilter(this.filter); + } else { + this.queryModel.removeFilter(this.filter); + } + } + + deactivate() { + if (this.useAsFilter) { + this.toggle(); + } + } + + reset() { + this.deactivate(); + this.filter.reset(); + } +} diff --git a/frontend/src/app/models/index.ts b/frontend/src/app/models/index.ts index 42f7c24da..53550146c 100644 --- a/frontend/src/app/models/index.ts +++ b/frontend/src/app/models/index.ts @@ -1,7 +1,9 @@ export * from './corpus'; export * from './found-document'; export * from './query'; -export * from './search-filter-old'; +export * from './search-filter'; +export * from './search-filter-options'; +export * from './filter-management'; export * from './search-results'; export * from './sort-event'; export * from './user'; diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index ef68408e6..c9247ca50 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -104,6 +104,18 @@ export class QueryModel { }); } + removeFilter(filter: SearchFilter) { + this.removeFiltersForField(filter.corpusField); + } + + removeFiltersForField(field: CorpusField) { + const filterIndex = () => this.filters.findIndex(filter => filter.corpusField.name === field.name); + while (filterIndex() !== -1) { + this.filters.splice(filterIndex()); + } + this.update.next(); + } + setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); From c5442eccc41a286c0b5a18ccb986fa648376dd73 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 15:54:28 +0100 Subject: [PATCH 082/262] start filter management --- .../app/filter/filter-manager.component.html | 32 ++-- .../filter/filter-manager.component.spec.ts | 28 ++-- .../app/filter/filter-manager.component.ts | 149 +++++------------- frontend/src/app/search/search.component.html | 2 +- 4 files changed, 71 insertions(+), 140 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index 372c2332d..0ee76513e 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -17,36 +17,38 @@

Filters

- -
+ +
-
-

- -

- - + + - - - + + - - + +
diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index da42a32c4..d234c6b66 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -6,6 +6,7 @@ import { FilterManagerComponent } from './filter-manager.component'; import { mockCorpus, mockCorpus2, mockFilter } from '../../mock-data/corpus'; import { convertToParamMap } from '@angular/router'; import { findByName } from '../utils/utils'; +import { QueryModel } from '../models'; describe('FilterManagerComponent', () => { let component: FilterManagerComponent; @@ -19,33 +20,30 @@ describe('FilterManagerComponent', () => { fixture = TestBed.createComponent(FilterManagerComponent); component = fixture.componentInstance; component.corpus = mockCorpus; + component.queryModel = new QueryModel(mockCorpus); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); - expect(component.searchFilters.length).toEqual(1); + expect(component.potentialFilters.length).toEqual(1); }); it('resets filters when corpus changes', () => { component.corpus = mockCorpus2; - component.initialize(); - expect(component.searchFilters.length).toEqual(0); + component.queryModel = new QueryModel(mockCorpus2); + fixture.detectChanges(); + expect(component.potentialFilters.length).toEqual(0); component.corpus = mockCorpus; - component.initialize(); - expect(component.searchFilters.length).toEqual(1); - }); - - it('parses parameters to filters', () => { - expect(component.activeFilters.length).toEqual(0); - const params = convertToParamMap({great_field: 'checked'}); - component.setStateFromParams(params); - expect(component.activeFilters.length).toEqual(1); + component.queryModel = new QueryModel(mockCorpus); + fixture.detectChanges(); + expect(component.potentialFilters.length).toEqual(1); }); it('toggles filters on and off', async() => { - findByName(component.corpusFields, 'great_field').searchFilter.useAsFilter = true; - const params = component.filtersChanged(); - expect(Object.keys(params)).toContain('great_field'); + const filter = component.potentialFilters.find(f => f.corpusField.name === 'great_field'); + expect(component.queryModel.filters.length).toBe(0); + component.toggleFilter(filter); + expect(component.queryModel.filters.length).toBe(1); }); }); diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 50453148f..7d2a31df1 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -1,72 +1,53 @@ +/* eslint-disable @typescript-eslint/member-ordering */ import { Component, Input, OnChanges } from '@angular/core'; -import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import * as _ from 'lodash'; import { Subject } from 'rxjs'; -import { AggregateData, Corpus, MultipleChoiceFilterData, SearchFilter, - SearchFilterData, CorpusField } from '../models/index'; +import { PotentialFilter, Corpus, SearchFilter, QueryModel, MultipleChoiceFilterOptions, AggregateData } from '../models/index'; import { SearchService } from '../services'; -import { ParamDirective } from '../param/param-directive'; -import { ParamService } from '../services/param.service'; -import { findByName } from '../utils/utils'; -import { filtersFromParams, paramForFieldName, searchFilterDataToParam } from '../utils/params'; - @Component({ selector: 'ia-filter-manager', templateUrl: './filter-manager.component.html', styleUrls: ['./filter-manager.component.scss'] }) -export class FilterManagerComponent extends ParamDirective implements OnChanges { +export class FilterManagerComponent implements OnChanges { @Input() public corpus: Corpus; + @Input() queryModel: QueryModel; inputChanged = new Subject(); - public corpusFields: CorpusField[]; - public searchFilters: SearchFilter [] = []; - public activeFilters: SearchFilter [] = []; + public potentialFilters: PotentialFilter[] = []; public showFilters: boolean; public grayOutFilters: boolean; - public multipleChoiceData: Object = {}; + public multipleChoiceData: { + [fieldName: string]: any[]; + } = {}; constructor( - private paramService: ParamService, - private searchService: SearchService, - route: ActivatedRoute, - router: Router) { - super(route, router); + private searchService: SearchService,) { } - initialize() { - this.corpusFields = _.cloneDeep(this.corpus.fields); - this.searchFilters = this.corpusFields.filter(field => field.searchFilter).map(field => field.searchFilter); + get activeFilters(): SearchFilter[] { + return this.queryModel.filters; } ngOnChanges() { - this.initialize(); + if (this.corpus && this.queryModel && !this.potentialFilters) { + this.potentialFilters = this.corpus.fields.map(field => new PotentialFilter(field, this.queryModel)); + this.queryModel.update.subscribe(this.onQueryModelUpdate.bind(this)); + } this.inputChanged.next(); } - setStateFromParams(params: ParamMap) { - this.activeFilters = filtersFromParams( - params, this.corpusFields - ); - this.aggregateSearchForMultipleChoiceFilters(params); + onQueryModelUpdate() { + this.aggregateSearchForMultipleChoiceFilters(); } - teardown() { - const params = {}; - this.activeFilters.forEach(filter => { - const paramName = paramForFieldName(filter.fieldName); - params[paramName] = null; - }); - this.setParams(params); - } - /** * For all multiple choice filters, get the bins and counts * Exclude the filter itself from the aggregate search @@ -74,105 +55,55 @@ export class FilterManagerComponent extends ParamDirective implements OnChanges * fieldName1: [{key: option1, doc_count: 42}, {key: option2, doc_count: 3}], * fieldName2: [etc] */ - private aggregateSearchForMultipleChoiceFilters(params) { - const multipleChoiceFilters = this.searchFilters.filter(f => !f.adHoc && f.currentData.filterType === 'MultipleChoiceFilter'); + private aggregateSearchForMultipleChoiceFilters() { + const multipleChoiceFilters = this.potentialFilters.filter(f => + f.corpusField.filterOptions?.name === 'MultipleChoiceFilter'); - const aggregateResultPromises = multipleChoiceFilters.map(filter => this.getMultipleChoiceFilterOptions(filter, params)); + const aggregateResultPromises = multipleChoiceFilters.map(filter => + this.getMultipleChoiceFilterOptions(filter)); Promise.all(aggregateResultPromises).then(results => { results.forEach( r => this.multipleChoiceData[Object.keys(r)[0]] = Object.values(r)[0] ); // if multipleChoiceData is empty, gray out all filters - if (multipleChoiceFilters && multipleChoiceFilters.length != 0) {this.grayOutFilters = this.multipleChoiceData[multipleChoiceFilters[0].fieldName].length === 0;} - }); - } - - async getMultipleChoiceFilterOptions(filter: SearchFilter, params: ParamMap): Promise { - let filters = _.cloneDeep(this.searchFilters.filter(f => f.useAsFilter === true)); - // get the filter's choices, based on all other filters' choices, but not this filter's choices - if (filters.length > 0) { - const index = filters.findIndex(f => f.fieldName === filter.fieldName); - if (index >= 0) { - filters.splice(index, 1); + if (multipleChoiceFilters && multipleChoiceFilters.length !== 0) { + this.grayOutFilters = this.multipleChoiceData[multipleChoiceFilters[0].corpusField.name].length === 0; } - } else { - filters = null; - } - const defaultData = filter.defaultData as MultipleChoiceFilterData; - const aggregator = {name: filter.fieldName, size: defaultData.optionCount}; - const queryModel = this.paramService.queryModelFromParams(params, this.corpusFields); - return this.searchService.aggregateSearch(this.corpus, queryModel, [aggregator]).then(results => { - return results.aggregations; - }, error => { - console.trace(error, aggregator); - return {}; }); } - toggleFilter(filter: SearchFilter) { - filter.useAsFilter = !filter.useAsFilter; - this.updateFilterData(filter); + private async getMultipleChoiceFilterOptions(filter: PotentialFilter): Promise { + const optionCount = (filter.corpusField.filterOptions as MultipleChoiceFilterOptions).option_count; + const aggregator = {name: filter.corpusField.name, size: optionCount}; + const queryModel = this.queryModel.clone(); + queryModel.removeFilter(filter.filter); // exclude the choices for this filter + return this.searchService.aggregateSearch(this.corpus, queryModel, [aggregator]).then( + response => response.aggregations); } - resetFilter(filter: SearchFilter) { - filter.useAsFilter = false; - filter.currentData = filter.defaultData; - filter.reset = true; - this.updateFilterData(filter); + toggleFilter(filter: PotentialFilter) { + filter.toggle(); } - /** - * Event triggered from filter components - * - * @param filterData - */ - public updateFilterData(filter: SearchFilter) { - findByName(this.corpusFields, filter.fieldName).searchFilter = filter; - this.filtersChanged(); + resetFilter(filter: PotentialFilter) { + filter.reset(); } public toggleActiveFilters() { if (this.activeFilters.length) { - this.activeFilters.forEach(filter => filter.useAsFilter = false); + this.potentialFilters.forEach(filter => filter.deactivate()); } else { // if we don't have active filters, set all filters to active which don't use default data - let filtersWithSettings = this.corpusFields.filter( - field => field.searchFilter && field.searchFilter.currentData != field.searchFilter.defaultData - ).map( field => field.searchFilter ); - filtersWithSettings.forEach( field => field.useAsFilter = true); + const filtersWithSettings = this.potentialFilters.filter(pFilter => + pFilter.filter.currentData === pFilter.filter.defaultData); + filtersWithSettings.forEach(filter => filter.toggle()); } - this.filtersChanged(); } public resetAllFilters() { this.activeFilters.forEach(filter => { - filter.currentData = filter.defaultData; - filter.reset = true; - }); - this.toggleActiveFilters(); - } - - public filtersChanged(): Object { - const newFilters = this.corpusFields.filter(field => field.searchFilter?.useAsFilter).map(f => f.searchFilter); - let params = {}; - this.activeFilters.forEach(filter => { - // set any params for previously active filters to null - if (!newFilters.map(f => f.fieldName).find(name => name === filter.fieldName)) { - const paramName = paramForFieldName(filter.fieldName); - params[paramName] = null; - if (filter.adHoc) { - // also set sort null in case of an adHoc filter - params['sort'] = null; - } - } - }); - newFilters.forEach(filter => { - const paramName = paramForFieldName(filter.fieldName); - const value = filter.useAsFilter? searchFilterDataToParam(filter) : null; - params[paramName] = value; + filter.reset(); }); - this.setParams(params); - return params; } } diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 64727bec7..5432bb63e 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -49,7 +49,7 @@
- +
From 324708ce5dc75fdeceb2acffb2ac7c24c477c25f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 17:01:04 +0100 Subject: [PATCH 083/262] remove paramservice, fix references to field.searchfilter --- frontend/src/app/app.module.ts | 3 +-- frontend/src/app/models/filter-management.ts | 5 ++++ frontend/src/app/models/search-filter.ts | 2 +- frontend/src/app/services/index.ts | 1 - .../src/app/services/param.service.spec.ts | 25 ------------------- frontend/src/app/services/param.service.ts | 16 ------------ .../barchart/histogram.component.ts | 18 +++++++------ .../barchart/timeline.component.ts | 15 ++++++----- 8 files changed, 26 insertions(+), 59 deletions(-) delete mode 100644 frontend/src/app/services/param.service.spec.ts delete mode 100644 frontend/src/app/services/param.service.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index da0812bc6..19858e294 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -27,7 +27,7 @@ import { PdfViewerModule } from 'ng2-pdf-viewer'; import { CookieService } from 'ngx-cookie-service'; import { ApiService, ApiRetryService, CorpusService, DialogService, DownloadService, - ElasticSearchService, ParamService, HighlightService, NotificationService, SearchService, SessionService, + ElasticSearchService, HighlightService, NotificationService, SearchService, SessionService, UserService, QueryService } from './services/index'; import { AppComponent } from './app.component'; @@ -280,7 +280,6 @@ export const providers: any[] = [ ElasticSearchService, HighlightService, NotificationService, - ParamService, QueryService, SearchService, SessionService, diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index 6533684e1..6f73d630e 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -1,6 +1,7 @@ import { CorpusField } from './corpus'; import { QueryModel } from './query'; import { SearchFilter } from './search-filter'; +import { SearchFilterType } from './search-filter-old'; export class PotentialFilter { filter: SearchFilter; @@ -17,6 +18,10 @@ export class PotentialFilter { } } + get filterType(): SearchFilterType { + return this.corpusField.filterOptions?.name; + } + toggle() { this.useAsFilter = !this.useAsFilter; if (this.useAsFilter) { diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index d28534298..25d034633 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -62,7 +62,7 @@ abstract class AbstractSearchFilter { abstract dataFromEsFilter(esFilter: EsFilterType): FilterData; } -interface DateFilterData { +export interface DateFilterData { min: Date; max: Date; } diff --git a/frontend/src/app/services/index.ts b/frontend/src/app/services/index.ts index 6174bb2db..9fdbdc269 100644 --- a/frontend/src/app/services/index.ts +++ b/frontend/src/app/services/index.ts @@ -4,7 +4,6 @@ export * from './corpus.service'; export * from './dialog.service'; export * from './download.service'; export * from './elastic-search.service'; -export * from './param.service'; export * from './highlight.service'; export * from './notification.service'; export * from './query.service'; diff --git a/frontend/src/app/services/param.service.spec.ts b/frontend/src/app/services/param.service.spec.ts deleted file mode 100644 index d21fde640..000000000 --- a/frontend/src/app/services/param.service.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { inject, TestBed } from '@angular/core/testing'; - -import { ParamService } from './param.service'; -import { SearchService } from './search.service'; -import { SearchServiceMock } from '../../mock-data/search'; -import { convertToParamMap } from '@angular/router'; - -describe('ParamService', () => { - let service: ParamService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - ParamService, - { provide: SearchService, useValue: new SearchServiceMock() } - ] - }); - service = TestBed.inject(ParamService); - }); - - it('should be created', inject([ParamService], (service: ParamService) => { - expect(service).toBeTruthy(); - })); - -}); diff --git a/frontend/src/app/services/param.service.ts b/frontend/src/app/services/param.service.ts deleted file mode 100644 index 395d57d3a..000000000 --- a/frontend/src/app/services/param.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as _ from 'lodash'; - -import { Injectable } from '@angular/core'; -import { ParamMap } from '@angular/router'; - -import { Corpus, CorpusField, FoundDocument, QueryModel } from '../models'; -import { SearchService } from './search.service'; - -@Injectable() -export class ParamService { - - constructor(private searchService: SearchService) { } - - - -} diff --git a/frontend/src/app/visualization/barchart/histogram.component.ts b/frontend/src/app/visualization/barchart/histogram.component.ts index 583489feb..6847288c9 100644 --- a/frontend/src/app/visualization/barchart/histogram.component.ts +++ b/frontend/src/app/visualization/barchart/histogram.component.ts @@ -1,11 +1,13 @@ import { Component, OnChanges, OnInit, SimpleChanges, } from '@angular/core'; import * as _ from 'lodash'; -import { AggregateResult, MultipleChoiceFilterData, RangeFilterData, +import { AggregateResult, HistogramSeries, QueryModel, HistogramDataPoint, - TermFrequencyResult} from '../../models/index'; + TermFrequencyResult, + MultipleChoiceFilterOptions, + RangeFilterOptions} from '../../models/index'; import { BarchartDirective } from './barchart.directive'; import { selectColor } from '../../utils/select-color'; @@ -31,15 +33,15 @@ export class HistogramComponent extends BarchartDirective im */ getAggregator() { let size = 0; - if (!this.visualizedField.searchFilter) { + if (!this.visualizedField.filterOptions) { return {name: this.visualizedField.name, size: 100}; } - const defaultData = this.visualizedField.searchFilter.defaultData; - if (defaultData.filterType === 'MultipleChoiceFilter') { - size = (defaultData as MultipleChoiceFilterData).optionCount; - } else if (defaultData.filterType === 'RangeFilter') { - size = (defaultData as RangeFilterData).max - (defaultData as RangeFilterData).min; + const filterOptions = this.visualizedField.filterOptions; + if (filterOptions.name === 'MultipleChoiceFilter') { + size = (filterOptions as MultipleChoiceFilterOptions).option_count; + } else if (filterOptions.name === 'RangeFilter') { + size = (filterOptions as RangeFilterOptions).upper - (filterOptions as RangeFilterOptions).lower; } return {name: this.visualizedField.name, size}; } diff --git a/frontend/src/app/visualization/barchart/timeline.component.ts b/frontend/src/app/visualization/barchart/timeline.component.ts index 5afcc2505..e7aed6ffc 100644 --- a/frontend/src/app/visualization/barchart/timeline.component.ts +++ b/frontend/src/app/visualization/barchart/timeline.component.ts @@ -3,8 +3,9 @@ import { Component, OnChanges, OnInit } from '@angular/core'; import * as d3TimeFormat from 'd3-time-format'; import * as _ from 'lodash'; -import { QueryModel, AggregateResult, TimelineSeries, DateFilterData, TimelineDataPoint, TermFrequencyResult, - TimeCategory } from '../../models/index'; +import { QueryModel, AggregateResult, TimelineSeries, TimelineDataPoint, TermFrequencyResult, + TimeCategory, + DateFilterData} from '../../models/index'; import { BarchartDirective } from './barchart.directive'; import * as moment from 'moment'; import 'chartjs-adapter-moment'; @@ -36,7 +37,9 @@ export class TimelineComponent extends BarchartDirective impl /** get min/max date for the entire graph and set domain and time category */ setTimeDomain() { - const currentDomain = this.visualizedField.searchFilter.currentData as DateFilterData; + const filter = this.queryModel.filters.find(f => f.corpusField.name === this.visualizedField.name) + || this.visualizedField.makeSearchFilter(); + const currentDomain = filter.currentData as DateFilterData; const min = new Date(currentDomain.min); const max = new Date(currentDomain.max); this.xDomain = [min, max]; @@ -265,11 +268,11 @@ export class TimelineComponent extends BarchartDirective impl /** * Add a date filter to a query model restricting it to the provided min and max values. */ - addQueryDateFilter(query: QueryModel, min, max): QueryModel { + addQueryDateFilter(query: QueryModel, min: Date, max: Date): QueryModel { const queryModelCopy = _.cloneDeep(query); // download zoomed in results - const filter = this.visualizedField.searchFilter; - filter.currentData = { filterType: 'DateFilter', min: this.timeFormat(min), max: this.timeFormat(max) }; + const filter = this.visualizedField.makeSearchFilter(); + filter.data.next({ min, max }); queryModelCopy.filters.push(filter); return queryModelCopy; } From 878ca4e374f1b34fdd7cdd6b00985abe9196402c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 17:44:49 +0100 Subject: [PATCH 084/262] draft updates to filter components --- .../src/app/filter/ad-hoc-filter.component.ts | 33 +++----------- .../src/app/filter/base-filter.component.ts | 45 ++++++++++--------- .../app/filter/boolean-filter.component.html | 4 +- .../filter/boolean-filter.component.spec.ts | 17 ++----- .../app/filter/boolean-filter.component.ts | 28 +++--------- .../src/app/filter/date-filter.component.html | 10 +++-- .../app/filter/date-filter.component.spec.ts | 23 +++------- .../src/app/filter/date-filter.component.ts | 39 ++++------------ .../multiple-choice-filter.component.spec.ts | 3 +- .../multiple-choice-filter.component.ts | 31 +++++++------ .../app/filter/range-filter.component.spec.ts | 20 +++------ .../src/app/filter/range-filter.component.ts | 28 +++++------- frontend/src/app/models/filter-management.ts | 6 +++ frontend/src/app/models/search-filter.ts | 2 +- frontend/src/mock-data/corpus.ts | 15 +++++++ 15 files changed, 118 insertions(+), 186 deletions(-) diff --git a/frontend/src/app/filter/ad-hoc-filter.component.ts b/frontend/src/app/filter/ad-hoc-filter.component.ts index bf17213d9..bf3198215 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.ts +++ b/frontend/src/app/filter/ad-hoc-filter.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { SearchFilter, SearchFilterData } from '../models'; +import { AdHocFilter, SearchFilter, SearchFilterData } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @Component({ @@ -7,34 +7,15 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './ad-hoc-filter.component.html', styleUrls: ['./ad-hoc-filter.component.scss'] }) -export class AdHocFilterComponent extends BaseFilterComponent implements OnInit { - data: { value: any} = { value: undefined }; +export class AdHocFilterComponent extends BaseFilterComponent { + data: any; - ngOnInit() { - if (this.filter) { - this.data = this.getDisplayData(this.filter); - } + getDisplayData(filter: AdHocFilter) { + return filter.currentData; } - getValue(data: SearchFilterData) { - switch (data.filterType) { - case 'BooleanFilter': - return data.checked; - case 'DateFilter': - return data.min; // can return either: min == max for ad hoc filters - case 'MultipleChoiceFilter': - return data.selected[0]; // only one value for ad hoc filters - case 'RangeFilter': - return data.min; - } - } - - getDisplayData(filter: SearchFilter) { - return { value: this.getValue( filter.currentData) }; - } - - getFilterData(): SearchFilter { - return undefined; + getFilterData(): any { + return this.data; } } diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index b6fef9f18..7c79c55b1 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -1,7 +1,7 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Subject } from 'rxjs'; -import { SearchFilter, SearchFilterData } from '../models/index'; +import { PotentialFilter, SearchFilter } from '../models/index'; /** * Filter component receives the corpus fields containing search filters as input @@ -10,51 +10,54 @@ import { SearchFilter, SearchFilterData } from '../models/index'; @Component({ template: '' }) -export abstract class BaseFilterComponent { +export abstract class BaseFilterComponent implements OnChanges { @Input() inputChanged: Subject; @Input() - public filter: SearchFilter; + public filter: PotentialFilter; @Input() public grayedOut: boolean; - @Output('update') public updateEmitter = new EventEmitter>(); - /** * The data of the applied filter transformed to use as input for the value editors. */ public data: any; // holds the user data - public useAsFilter = false; - constructor() { } + ngOnChanges(changes): void { + if (changes.filter) { + this.filter.filter.data.subscribe(this.provideFilterData.bind(this)); + } + } + provideFilterData() { if (this.filter) { - this.data = this.getDisplayData(this.filter); - this.useAsFilter = this.filter.useAsFilter; + this.data = this.getDisplayData(this.filter.filter as SearchFilterClass); } } - abstract getDisplayData(filter: SearchFilter); - - /** - * Create a new version of the filter data from the user input. - */ - abstract getFilterData(): SearchFilter; - /** * Trigger a change event. */ update() { + const data = this.getFilterData(); + this.filter.filter.data.next(data); if (this.data.selected && this.data.selected.length === 0) { - this.useAsFilter = false; + this.filter.deactivate(); } else { - this.useAsFilter = true; // update called through user input + this.filter.activate(); // update called through user input } - this.filter.useAsFilter = this.useAsFilter; - this.updateEmitter.emit(this.getFilterData()); } + + + abstract getDisplayData(filter: SearchFilterClass); + + /** + * Create a new version of the filter data from the user input. + */ + abstract getFilterData(): typeof this.filter.filter.currentData; + } diff --git a/frontend/src/app/filter/boolean-filter.component.html b/frontend/src/app/filter/boolean-filter.component.html index 643de4523..62a871302 100644 --- a/frontend/src/app/filter/boolean-filter.component.html +++ b/frontend/src/app/filter/boolean-filter.component.html @@ -1,4 +1,4 @@
- - {{data.checked | json | titlecase }} + + {{data | titlecase }}
diff --git a/frontend/src/app/filter/boolean-filter.component.spec.ts b/frontend/src/app/filter/boolean-filter.component.spec.ts index 352d7d5d7..37b819484 100644 --- a/frontend/src/app/filter/boolean-filter.component.spec.ts +++ b/frontend/src/app/filter/boolean-filter.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { mockCorpus3, mockField } from 'src/mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; +import { PotentialFilter, QueryModel } from '../models'; import { BooleanFilterComponent } from './boolean-filter.component'; @@ -15,19 +17,8 @@ describe('BooleanFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(BooleanFilterComponent); component = fixture.componentInstance; - component.filter = { - fieldName: 'A yes/no question', - description: 'What is the average speed of a swallow?', - useAsFilter: false, - defaultData: { - filterType: 'BooleanFilter', - checked: false - }, - currentData: { - filterType: 'BooleanFilter', - checked: true - } - }; + const query = new QueryModel(mockCorpus3); + component.filter = new PotentialFilter(mockField, query); fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/boolean-filter.component.ts b/frontend/src/app/filter/boolean-filter.component.ts index ed83081dd..d12a4d360 100644 --- a/frontend/src/app/filter/boolean-filter.component.ts +++ b/frontend/src/app/filter/boolean-filter.component.ts @@ -1,37 +1,23 @@ import { Component, DoCheck, OnInit } from '@angular/core'; import { BaseFilterComponent } from './base-filter.component'; -import { SearchFilter, BooleanFilterData } from '../models'; +import { BooleanFilter } from '../models'; @Component({ selector: 'ia-boolean-filter', templateUrl: './boolean-filter.component.html', styleUrls: ['./boolean-filter.component.scss'] }) -export class BooleanFilterComponent extends BaseFilterComponent implements DoCheck, OnInit { +export class BooleanFilterComponent extends BaseFilterComponent { + data: boolean; - ngOnInit() { - this.provideFilterData(); - } - - ngDoCheck() { - if (this.filter.reset) { - this.filter.reset = false; - this.provideFilterData(); - } - } - - getDisplayData(filter: SearchFilter) { + getDisplayData(filter: BooleanFilter) { const data = filter.currentData; - return { checked: data.checked }; + return data; } - getFilterData(): SearchFilter { - this.filter.currentData = { - filterType: 'BooleanFilter', - checked: this.data.checked - }; - return this.filter; + getFilterData(): boolean { + return this.data; } } diff --git a/frontend/src/app/filter/date-filter.component.html b/frontend/src/app/filter/date-filter.component.html index b73288f17..34feec72d 100644 --- a/frontend/src/app/filter/date-filter.component.html +++ b/frontend/src/app/filter/date-filter.component.html @@ -1,6 +1,8 @@
- - + +
diff --git a/frontend/src/app/filter/date-filter.component.spec.ts b/frontend/src/app/filter/date-filter.component.spec.ts index 1ccabf96f..fa4e3ada5 100644 --- a/frontend/src/app/filter/date-filter.component.spec.ts +++ b/frontend/src/app/filter/date-filter.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { mockCorpus3, mockFieldDate } from 'src/mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; +import { PotentialFilter, QueryModel } from '../models'; import { DateFilterComponent } from './date-filter.component'; @@ -15,24 +17,11 @@ describe('DateFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DateFilterComponent); component = fixture.componentInstance; - component.filter = { - fieldName: 'Publication date', - description: 'When this book was published', - useAsFilter: false, - defaultData: { - filterType: 'DateFilter', - min: '1099-01-01', - max: '1300-12-31' - }, - currentData: { - filterType: 'DateFilter', - min: '1111-01-01', - max: '1299-12-31' - } - }; + const queryModel = new QueryModel(mockCorpus3); + component.filter = new PotentialFilter(mockFieldDate, queryModel); component.data = { - minYear: 1099, - maxYear: 1300 + min: new Date('Jan 1 1810'), + max: new Date('Dec 31 1820') }; fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/date-filter.component.ts b/frontend/src/app/filter/date-filter.component.ts index 762f5054f..70060aa36 100644 --- a/frontend/src/app/filter/date-filter.component.ts +++ b/frontend/src/app/filter/date-filter.component.ts @@ -1,8 +1,8 @@ -import { Component, DoCheck, OnChanges, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import * as moment from 'moment'; -import { SearchFilter, DateFilterData } from '../models'; +import { DateFilterData, DateFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @Component({ @@ -10,7 +10,7 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './date-filter.component.html', styleUrls: ['./date-filter.component.scss'] }) -export class DateFilterComponent extends BaseFilterComponent implements DoCheck, OnInit { +export class DateFilterComponent extends BaseFilterComponent implements OnInit { public minDate: Date; public maxDate: Date; public minYear: number; @@ -18,47 +18,24 @@ export class DateFilterComponent extends BaseFilterComponent imp ngOnInit() { this.provideFilterData(); - this.minDate = new Date(this.filter.defaultData.min); - this.maxDate = new Date(this.filter.defaultData.max); + this.minDate = this.filter.filter.defaultData.min; + this.maxDate = this.filter.filter.defaultData.max; this.minYear = this.minDate.getFullYear(); this.maxYear = this.maxDate.getFullYear(); } - ngDoCheck() { - if (this.filter.reset) { - this.filter.reset = false; - this.provideFilterData(); - } - } - - - getDisplayData(filter: SearchFilter) { + getDisplayData(filter: DateFilter) { const data = filter.currentData; return { min: new Date(data.min), max: new Date(data.max), - minYear: this.minYear, - maxYear: this.maxYear }; } /** * Create a new version of the filter data from the user input. */ - getFilterData(): SearchFilter { - this.filter.currentData = { - filterType: 'DateFilter', - min: this.formatDate(this.data.min), - max: this.formatDate(this.data.max) - }; - return this.filter; + getFilterData(): DateFilterData { + return this.data; } - - /** - * Return a string of the form 0123-04-25. - */ - formatDate(date: Date): string { - return moment(date).format().slice(0, 10); - } - } diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index 24593bf9a..b9e8a11de 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -17,7 +17,8 @@ describe('MultipleChoiceFilterComponent', () => { component = fixture.componentInstance; component.optionsAndCounts = [{key: 'Andy', doc_count: 2}, {key: 'Lou', doc_count: 3}]; component.data = { - options: ['Andy', 'Lou'] + options: ['Andy', 'Lou'], + selected: [], }; fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index 812301e67..53dbaaf24 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -3,22 +3,25 @@ import { Component, Input, OnInit, OnChanges } from '@angular/core'; import * as _ from 'lodash'; import { BaseFilterComponent } from './base-filter.component'; -import { SearchFilter, MultipleChoiceFilterData, AggregateResult } from '../models'; +import { SearchFilter, AggregateResult, PotentialFilter, MultipleChoiceFilter } from '../models'; @Component({ selector: 'ia-multiple-choice-filter', templateUrl: './multiple-choice-filter.component.html', styleUrls: ['./multiple-choice-filter.component.scss'] }) -export class MultipleChoiceFilterComponent extends BaseFilterComponent implements OnChanges { +export class MultipleChoiceFilterComponent extends BaseFilterComponent { + @Input() potentialFilter: PotentialFilter; @Input() public optionsAndCounts: AggregateResult[]; - ngOnChanges() { - this.provideFilterData(); - } + data: { + options: string[]; + selected: string[]; + } = { + options: [], selected: [] + }; - getDisplayData(filter: SearchFilter) { - this.data = filter.currentData; + getDisplayData(filter: MultipleChoiceFilter) { let options = []; if (this.optionsAndCounts) { options = _.sortBy( @@ -26,17 +29,13 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent o.label ); } else { -options = [1, 2, 3]; -} // dummy array to make sure the component loads - return { options, selected: this.data.selected }; + options = [1, 2, 3]; + } // dummy array to make sure the component loads + return { options, selected: filter.currentData }; } - getFilterData(): SearchFilter { - this.filter.currentData = { - filterType: 'MultipleChoiceFilter', - selected: this.data.selected - }; - return this.filter; + getFilterData(): string[] { + return this.data.selected; } diff --git a/frontend/src/app/filter/range-filter.component.spec.ts b/frontend/src/app/filter/range-filter.component.spec.ts index 000885e7b..83debb08b 100644 --- a/frontend/src/app/filter/range-filter.component.spec.ts +++ b/frontend/src/app/filter/range-filter.component.spec.ts @@ -3,7 +3,8 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { commonTestBed } from '../common-test-bed'; import { RangeFilterComponent } from './range-filter.component'; -import { RangeFilterData } from '../models'; +import { PotentialFilter, QueryModel } from '../models'; +import { mockCorpus3, mockField3 } from 'src/mock-data/corpus'; describe('RangeFilterComponent', () => { let component: RangeFilterComponent; @@ -16,20 +17,9 @@ describe('RangeFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(RangeFilterComponent); component = fixture.componentInstance; - const mockRangeData = { - filterType: 'RangeFilter', - min: 1984, - max: 1984 - } as RangeFilterData; - const data = { - fieldName: 'year', - description: 'Orwellian', - useAsFilter: false, - defaultData: mockRangeData, - currentData: mockRangeData - }; - component.filter = data; - component.data = data; + const query = new QueryModel(mockCorpus3); + component.filter = new PotentialFilter(mockField3, query); + component.data = [1984, 1984]; fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/range-filter.component.ts b/frontend/src/app/filter/range-filter.component.ts index 86d294b38..290dee09b 100644 --- a/frontend/src/app/filter/range-filter.component.ts +++ b/frontend/src/app/filter/range-filter.component.ts @@ -1,6 +1,6 @@ -import { Component, DoCheck, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; -import { SearchFilter, RangeFilterData } from '../models'; +import { SearchFilter, RangeFilterData, RangeFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @Component({ @@ -8,30 +8,22 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './range-filter.component.html', styleUrls: ['./range-filter.component.scss'] }) -export class RangeFilterComponent extends BaseFilterComponent implements DoCheck, OnInit { +export class RangeFilterComponent extends BaseFilterComponent implements OnInit { + data: [number, number]; + ngOnInit() { this.provideFilterData(); } - ngDoCheck() { - if (this.filter.reset) { - this.filter.reset = false; - this.provideFilterData(); - } - } - - getDisplayData(filter: SearchFilter) { - this.data = filter.currentData; - return [this.data.min, this.data.max]; + getDisplayData(filter: RangeFilter) { + return [filter.currentData.min, filter.currentData.max]; } - getFilterData(): SearchFilter { - this.filter.currentData = { - filterType: 'RangeFilter', + getFilterData(): RangeFilterData { + return { min: this.data[0], - max: this.data[1] + max: this.data[1], }; - return this.filter; } } diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index 6f73d630e..11712bc58 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -37,6 +37,12 @@ export class PotentialFilter { } } + activate() { + if (!this.useAsFilter) { + this.toggle(); + } + } + reset() { this.deactivate(); this.filter.reset(); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 25d034633..49f7273c1 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -188,7 +188,7 @@ export class MultipleChoiceFilter extends AbstractSearchFilter(mockCorpus); public currentCorpus = this.currentCorpusSubject.asObservable(); From e43e6b25e89833d62446a0a1c48dd2345fbe97ab Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 17:56:24 +0100 Subject: [PATCH 085/262] draft update for search sorting component --- frontend/src/app/models/query.ts | 8 +++- .../app/search/search-results.component.html | 3 +- .../app/search/search-sorting.component.ts | 45 ++++++++----------- frontend/src/app/search/search.component.ts | 9 ++-- .../select-field/select-field.component.ts | 2 +- 5 files changed, 31 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index c9247ca50..2b4c9486a 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -84,7 +84,7 @@ export class QueryModel { } /** sort direction to be used in searching: replaces 'default' with the default value */ - private get actualSortBy(): CorpusField|'relevance' { + get actualSortBy(): CorpusField|'relevance' { if (this.sortBy !== 'default') { return this.sortBy; } else { @@ -116,6 +116,12 @@ export class QueryModel { this.update.next(); } + setSort(sortBy: SortBy, sortDirection: SortDirection) { + this.sortBy = sortBy; + this.sortDirection = sortDirection; + this.update.next(); + } + setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); diff --git a/frontend/src/app/search/search-results.component.html b/frontend/src/app/search/search-results.component.html index 7e0e2614c..596cec682 100644 --- a/frontend/src/app/search/search-results.component.html +++ b/frontend/src/app/search/search-results.component.html @@ -23,8 +23,7 @@

Sort By

- - +
diff --git a/frontend/src/app/search/search-sorting.component.ts b/frontend/src/app/search/search-sorting.component.ts index 1e41cdf4d..c0ec2a924 100644 --- a/frontend/src/app/search/search-sorting.component.ts +++ b/frontend/src/app/search/search-sorting.component.ts @@ -1,9 +1,5 @@ -import { Component, Input } from '@angular/core'; -import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CorpusField, QueryModel } from '../models'; -import { ParamDirective } from '../param/param-directive'; -import { sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; -import { sortDirectionFromBoolean } from '../utils/sort'; const defaultValueType = 'alpha'; @Component({ @@ -12,16 +8,9 @@ const defaultValueType = 'alpha'; styleUrls: ['./search-sorting.component.scss'], host: { class: 'field has-addons' } }) -export class SearchSortingComponent extends ParamDirective { - @Input() - public set fields(fields: CorpusField[]) { - this.sortableFields = fields.filter(field => field.sortable); - } +export class SearchSortingComponent implements OnChanges { + @Input() queryModel: QueryModel; - private sortData: { - field: CorpusField - ascending: boolean - } public ascending = true; public primarySort: CorpusField; public sortField: CorpusField; @@ -30,23 +19,28 @@ export class SearchSortingComponent extends ParamDirective { public sortableFields: CorpusField[]; public showFields = false; + private sortData: { + field: CorpusField; + ascending: boolean; + }; + + + constructor() {} + public get sortType(): SortType { return `${this.valueType}${this.ascending ? 'Asc' : 'Desc'}` as SortType; } - initialize() { - this.primarySort = this.sortableFields.find(field => field.primarySort); - this.sortField = this.primarySort; - } - teardown() { - this.setParams({ sort: null }); + ngOnChanges(changes: SimpleChanges): void { + if (changes.queryModel) { + this.queryModel.update.subscribe(this.setStateFromQueryModel.bind(this)); + } } - setStateFromParams(params: ParamMap) { - this.sortData = sortSettingsFromParams(params, this.sortableFields); - this.sortField = this.sortData.field; - this.ascending = this.sortData.ascending; + setStateFromQueryModel(queryModel: QueryModel) { + this.sortField = (queryModel.actualSortBy as CorpusField); + this.ascending = queryModel.sortDirection === 'asc'; } public toggleSortType() { @@ -70,8 +64,7 @@ export class SearchSortingComponent extends ParamDirective { } private updateSort() { - const setting = sortSettingsToParams(this.sortField, sortDirectionFromBoolean(this.ascending)); - this.setParams(setting); + this.queryModel.setSort(this.sortField, this.ascending? 'asc': 'desc'); } } diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index d8ecd23f0..8a99a310d 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -1,11 +1,9 @@ -import {Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { Component, ElementRef, ViewChild, HostListener } from '@angular/core'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; -import * as _ from 'lodash'; -import { Corpus, CorpusField, ResultOverview, QueryModel, User, SearchFilter, - FoundDocument} from '../models/index'; -import { CorpusService, DialogService, ParamService, SearchService } from '../services/index'; +import { Corpus, CorpusField, ResultOverview, QueryModel, User } from '../models/index'; +import { CorpusService, DialogService, } from '../services/index'; import { ParamDirective } from '../param/param-directive'; import { makeContextParams } from '../utils/document-context'; import { AuthService } from '../services/auth.service'; @@ -55,7 +53,6 @@ export class SearchComponent extends ParamDirective { constructor( private authService: AuthService, private corpusService: CorpusService, - private paramService: ParamService, private dialogService: DialogService, route: ActivatedRoute, router: Router diff --git a/frontend/src/app/select-field/select-field.component.ts b/frontend/src/app/select-field/select-field.component.ts index ee3fd1315..9727712b4 100644 --- a/frontend/src/app/select-field/select-field.component.ts +++ b/frontend/src/app/select-field/select-field.component.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash'; import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { CorpusField } from '../models/index'; +import { CorpusField, QueryModel } from '../models/index'; import { ParamDirective } from '../param/param-directive'; import { searchFieldsFromParams } from '../utils/params'; From bdd4e3f6181938a32357a76f0f969dd4ceba8788 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:01:14 +0100 Subject: [PATCH 086/262] update search field selection for barchart --- .../barchart/barchart.directive.ts | 7 +- .../barchart/histogram.component.spec.ts | 81 ++++--------------- frontend/src/mock-data/corpus.ts | 2 +- 3 files changed, 19 insertions(+), 71 deletions(-) diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts index f2dc8a744..fd9a7cf1a 100644 --- a/frontend/src/app/visualization/barchart/barchart.directive.ts +++ b/frontend/src/app/visualization/barchart/barchart.directive.ts @@ -609,12 +609,9 @@ export abstract class BarchartDirective get searchFields(): string { if (this.corpus && this.queryModel) { - const searchFields = this.selectSearchFields(this.queryModel).fields; + const searchFields = this.selectSearchFields(this.queryModel).searchFields; - const displayNames = searchFields.map(fieldName => { - const field = findByName(this.corpus.fields, fieldName); - return field.displayName; - }); + const displayNames = searchFields.map(field => field.displayName); return displayNames.join(', '); } diff --git a/frontend/src/app/visualization/barchart/histogram.component.spec.ts b/frontend/src/app/visualization/barchart/histogram.component.spec.ts index 679c2caa1..46494cd8d 100644 --- a/frontend/src/app/visualization/barchart/histogram.component.spec.ts +++ b/frontend/src/app/visualization/barchart/histogram.component.spec.ts @@ -1,63 +1,10 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { Corpus } from '../../models'; +import { QueryModel } from '../../models'; import { commonTestBed } from '../../common-test-bed'; import { HistogramComponent } from './histogram.component'; - -const MOCK_CORPUS: Corpus = { - name: 'mock-corpus', - serverName: 'bogus', - title: 'Mock Corpus', - description: 'corpus for testing', - index: 'mock-corpus', - image: 'nothing', - minDate: new Date('1-1-1800'), - maxDate: new Date('1-1-2000'), - scan_image_type: 'nothing', - allow_image_download: false, - word_models_present: false, - fields: [ - { - name: 'content', - description: 'main content field', - displayName: 'Content', - displayType: 'text_content', - mappingType: 'text', - searchable: true, - downloadable: true, - searchFilter: undefined, - primarySort: false, - sortable: false, - hidden: false, - }, { - name: 'keyword-1', - description: 'a keyword field', - displayName: 'Keyword 1', - displayType: 'keyword', - mappingType: 'keyword', - searchable: true, - downloadable: true, - searchFilter: undefined, - primarySort: false, - sortable: false, - hidden: false, - }, { - name: 'text', - description: 'a text field', - displayName: 'Text', - displayType: 'text', - mappingType: 'text', - searchable: true, - downloadable: true, - searchFilter: undefined, - primarySort: false, - sortable: false, - hidden: false, - } - ] -}; - +import { mockCorpus3, mockField, mockField2 } from '../../../mock-data/corpus'; describe('HistogramCompoment', () => { let component: HistogramComponent; @@ -79,32 +26,36 @@ describe('HistogramCompoment', () => { it('should filter text fields', () => { - component.corpus = MOCK_CORPUS; + component.corpus = mockCorpus3; component.frequencyMeasure = 'documents'; + const query1 = new QueryModel(mockCorpus3); + query1.setQueryText('test'); + + const query2 = new QueryModel(mockCorpus3); + query2.setQueryText('test'); + query2.searchFields = [mockField, mockField2]; + const cases = [ { - query: { - queryText: 'test' - } + query: query1, + searchFields: undefined, }, { - query: { - queryText: 'test', - fields: ['content', 'text'], - } + query: query2, + searchFields: [mockField, mockField2] } ]; cases.forEach(testCase => { const newQuery = component.selectSearchFields(testCase.query); - expect(newQuery.fields).toEqual(testCase.query.fields); + expect(newQuery.searchFields).toEqual(testCase.query.searchFields); }); component.frequencyMeasure = 'tokens'; cases.forEach(testCase => { const newQuery = component.selectSearchFields(testCase.query); - expect(newQuery.fields).toEqual(['content']); + expect(newQuery.searchFields).toEqual([mockField2]); }); }); diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index 45348b024..1ff3816cd 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -37,7 +37,7 @@ export const mockField2 = new CorpusField({ name: 'speech', description: 'A content field', display_name: 'Speechiness', - display_type: 'text', + display_type: 'text_content', es_mapping: {type: 'text'}, hidden: false, sortable: false, From 3f8cdd01ae95778cb6f35555a42d404a9dfe5339 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:09:34 +0100 Subject: [PATCH 087/262] sync highlight component with querymodel --- frontend/src/app/models/query.ts | 5 ++++ .../search/highlight-selector.component.ts | 28 ++++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 2b4c9486a..8bcb38bdc 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -122,6 +122,11 @@ export class QueryModel { this.update.next(); } + setHighlight(size?: number) { + this.highlightSize = size; + this.update.next(); + } + setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); diff --git a/frontend/src/app/search/highlight-selector.component.ts b/frontend/src/app/search/highlight-selector.component.ts index 31ccb901b..064859f0e 100644 --- a/frontend/src/app/search/highlight-selector.component.ts +++ b/frontend/src/app/search/highlight-selector.component.ts @@ -1,8 +1,6 @@ -import { Component } from '@angular/core'; -import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { QueryModel } from '../models'; -import { ParamDirective } from '../param/param-directive'; -import { highlightFromParams } from '../utils/params'; const HIGHLIGHT = 200; @@ -11,29 +9,27 @@ const HIGHLIGHT = 200; templateUrl: './highlight-selector.component.html', styleUrls: ['./highlight-selector.component.scss'] }) -export class HighlightSelectorComponent extends ParamDirective { +export class HighlightSelectorComponent implements OnChanges { + @Input() queryModel: QueryModel; public highlight: number = HIGHLIGHT; - constructor(route: ActivatedRoute, router: Router) { - super(route, router); + constructor() { } - initialize() { - - } - - teardown() { - this.setParams({ highlight: null }); + ngOnChanges(changes: SimpleChanges): void { + if (changes.queryModel) { + this.queryModel.update.subscribe(this.setStateFromQueryModel.bind(this)); + } } - setStateFromParams(params: ParamMap) { - this.highlight = highlightFromParams(params); + setStateFromQueryModel() { + this.highlight = this.queryModel.highlightSize; } updateHighlightSize(event) { const highlightSize = event.target.value; - this.setParams({ highlight: highlightSize !== "0" ? highlightSize : null }); + this.queryModel.setHighlight(highlightSize); } } From a9155a62f5d2c0a80920939fe0c5a2eb9a50f4fc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:22:41 +0100 Subject: [PATCH 088/262] remove unused references to searchfilterdata --- frontend/src/app/filter/ad-hoc-filter.component.ts | 4 ++-- frontend/src/app/services/corpus.service.spec.ts | 5 +---- frontend/src/app/services/corpus.service.ts | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/filter/ad-hoc-filter.component.ts b/frontend/src/app/filter/ad-hoc-filter.component.ts index bf3198215..27b9c1e1d 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.ts +++ b/frontend/src/app/filter/ad-hoc-filter.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { AdHocFilter, SearchFilter, SearchFilterData } from '../models'; +import { Component } from '@angular/core'; +import { AdHocFilter, } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @Component({ diff --git a/frontend/src/app/services/corpus.service.spec.ts b/frontend/src/app/services/corpus.service.spec.ts index 88155b700..7e27ceac7 100644 --- a/frontend/src/app/services/corpus.service.spec.ts +++ b/frontend/src/app/services/corpus.service.spec.ts @@ -1,4 +1,4 @@ -import { TestBed, inject, fakeAsync } from '@angular/core/testing'; +import { TestBed, inject } from '@angular/core/testing'; import { ApiServiceMock } from '../../mock-data/api'; import { ApiService } from './api.service'; @@ -8,10 +8,7 @@ import { UserService } from './user.service'; import { UserServiceMock } from '../../mock-data/user'; import { SessionService } from './session.service'; -import { Corpus } from '../models/corpus'; -import { CorpusField, SearchFilterData } from '../models/index'; import { RouterTestingModule } from '@angular/router/testing'; -import { Router } from '@angular/router'; import * as _ from 'lodash'; describe('CorpusService', () => { diff --git a/frontend/src/app/services/corpus.service.ts b/frontend/src/app/services/corpus.service.ts index fe4e4c47f..da6531192 100644 --- a/frontend/src/app/services/corpus.service.ts +++ b/frontend/src/app/services/corpus.service.ts @@ -2,14 +2,11 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import * as moment from 'moment'; import { Corpus, CorpusField, - DocumentContext, - SearchFilter, - SearchFilterData, + DocumentContext } from '../models/index'; import { ApiRetryService } from './api-retry.service'; import { AuthService } from './auth.service'; From b44694ff8b9425d256d616e8340171f446ad5369 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:26:07 +0100 Subject: [PATCH 089/262] fix reference in download history --- .../download-history/download-history.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/history/download-history/download-history.component.ts b/frontend/src/app/history/download-history/download-history.component.ts index b7e37ad54..b03d2d9c2 100644 --- a/frontend/src/app/history/download-history/download-history.component.ts +++ b/frontend/src/app/history/download-history/download-history.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { faDownload } from '@fortawesome/free-solid-svg-icons'; import * as _ from 'lodash'; -import { Corpus, Download, DownloadOptions, DownloadParameters, DownloadType, QueryModel } from '../../models'; -import { ApiService, CorpusService, DownloadService, ElasticSearchService, EsQuery, NotificationService } from '../../services'; +import { esQueryToQueryModel } from '../../utils/es-query'; +import { Download, DownloadOptions, DownloadParameters, DownloadType, QueryModel } from '../../models'; +import { ApiService, CorpusService, DownloadService, NotificationService } from '../../services'; import { HistoryDirective } from '../history.directive'; import { findByName } from '../../utils/utils'; @@ -22,7 +23,6 @@ export class DownloadHistoryComponent extends HistoryDirective implements OnInit private downloadService: DownloadService, private apiService: ApiService, corpusService: CorpusService, - private elasticSearchService: ElasticSearchService, private notificationService: NotificationService ) { super(corpusService); @@ -56,7 +56,7 @@ export class DownloadHistoryComponent extends HistoryDirective implements OnInit const esQueries = 'es_query' in parameters ? [parameters.es_query] : parameters.map(p => p.es_query); const corpus = findByName(this.corpora, download.corpus); - return esQueries.map(esQuery => this.elasticSearchService.esQueryToQueryModel(esQuery, corpus)); + return esQueries.map(esQuery => esQueryToQueryModel(esQuery, corpus)); } From 5d757485697dfbd3cd121fd7db5a68efe9026929 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:34:08 +0100 Subject: [PATCH 090/262] fix import in filter tests --- frontend/src/app/filter/boolean-filter.component.spec.ts | 2 +- frontend/src/app/filter/date-filter.component.spec.ts | 2 +- frontend/src/app/filter/range-filter.component.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/filter/boolean-filter.component.spec.ts b/frontend/src/app/filter/boolean-filter.component.spec.ts index 37b819484..2d1909ff9 100644 --- a/frontend/src/app/filter/boolean-filter.component.spec.ts +++ b/frontend/src/app/filter/boolean-filter.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { mockCorpus3, mockField } from 'src/mock-data/corpus'; +import { mockCorpus3, mockField } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; import { PotentialFilter, QueryModel } from '../models'; diff --git a/frontend/src/app/filter/date-filter.component.spec.ts b/frontend/src/app/filter/date-filter.component.spec.ts index fa4e3ada5..1b3efbf47 100644 --- a/frontend/src/app/filter/date-filter.component.spec.ts +++ b/frontend/src/app/filter/date-filter.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { mockCorpus3, mockFieldDate } from 'src/mock-data/corpus'; +import { mockCorpus3, mockFieldDate } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; import { PotentialFilter, QueryModel } from '../models'; diff --git a/frontend/src/app/filter/range-filter.component.spec.ts b/frontend/src/app/filter/range-filter.component.spec.ts index 83debb08b..cfaeef80e 100644 --- a/frontend/src/app/filter/range-filter.component.spec.ts +++ b/frontend/src/app/filter/range-filter.component.spec.ts @@ -4,7 +4,7 @@ import { commonTestBed } from '../common-test-bed'; import { RangeFilterComponent } from './range-filter.component'; import { PotentialFilter, QueryModel } from '../models'; -import { mockCorpus3, mockField3 } from 'src/mock-data/corpus'; +import { mockCorpus3, mockField3 } from '../../mock-data/corpus'; describe('RangeFilterComponent', () => { let component: RangeFilterComponent; From e3d6984a21bb9ccac8e8213278b94bee78a6e467 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:38:02 +0100 Subject: [PATCH 091/262] update query model in ngram spec --- .../app/visualization/ngram/ngram.component.spec.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/visualization/ngram/ngram.component.spec.ts b/frontend/src/app/visualization/ngram/ngram.component.spec.ts index c4e291c12..5f1521637 100644 --- a/frontend/src/app/visualization/ngram/ngram.component.spec.ts +++ b/frontend/src/app/visualization/ngram/ngram.component.spec.ts @@ -1,5 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { convertToParamMap, Params } from '@angular/router'; +import { convertToParamMap } from '@angular/router'; +import { QueryModel } from '../../models'; +import { mockCorpus } from '../../../mock-data/corpus'; import { MockCorpusResponse } from '../../../mock-data/corpus-response'; import { commonTestBed } from '../../common-test-bed'; import { NgramComponent } from './ngram.component'; @@ -15,10 +17,9 @@ describe('NgramComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(NgramComponent); component = fixture.componentInstance; - component.queryModel = { - queryText: 'testing', - filters: [] - }; + const queryModel = new QueryModel(mockCorpus); + queryModel.setQueryText('testing'); + component.queryModel = queryModel; component.corpus = MockCorpusResponse[0] as any; component.visualizedField = {name: 'speech'} as any; component.asTable = false; From 204e0879509c229df2106b75aa10d9c54f1f41bc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:40:43 +0100 Subject: [PATCH 092/262] fix more querymodel stuff in barchart --- .../visualization/barchart/barchart.directive.ts | 15 ++++++++------- .../visualization/barchart/timeline.component.ts | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts index fd9a7cf1a..1d37be71a 100644 --- a/frontend/src/app/visualization/barchart/barchart.directive.ts +++ b/frontend/src/app/visualization/barchart/barchart.directive.ts @@ -4,9 +4,11 @@ import { Directive, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChange import * as _ from 'lodash'; import { ApiService, NotificationService, SearchService } from '../../services/index'; -import { Chart, ChartOptions, ChartType } from 'chart.js'; -import { AggregateResult, BarchartResult, Corpus, FreqTableHeaders, QueryModel, CorpusField, TaskResult, - BarchartSeries, AggregateQueryFeedback, TimelineDataPoint, HistogramDataPoint, TermFrequencyResult, ChartParameters } from '../../models'; +import { Chart, ChartOptions } from 'chart.js'; +import { + AggregateResult, Corpus, FreqTableHeaders, QueryModel, CorpusField, TaskResult, + BarchartSeries, AggregateQueryFeedback, TimelineDataPoint, HistogramDataPoint, TermFrequencyResult, ChartParameters +} from '../../models'; import Zoom from 'chartjs-plugin-zoom'; import { BehaviorSubject } from 'rxjs'; import { selectColor } from '../../utils/select-color'; @@ -276,9 +278,8 @@ export abstract class BarchartDirective } else { const mainContentFields = this.corpus.fields.filter(field => field.searchable && (field.displayType === 'text_content')); - const queryModelCopy = _.cloneDeep(queryModel); - queryModelCopy.fields = mainContentFields.map(field => field.name); - + const queryModelCopy = queryModel.clone(); + queryModelCopy.searchFields = mainContentFields; return queryModelCopy; } } @@ -564,7 +565,7 @@ export abstract class BarchartDirective /** return a copy of a query model with the query text set to the given value */ setQueryText(query: QueryModel, queryText: string): QueryModel { - const queryModelCopy = _.cloneDeep(query); + const queryModelCopy = query.clone(); queryModelCopy.queryText = queryText; return queryModelCopy; } diff --git a/frontend/src/app/visualization/barchart/timeline.component.ts b/frontend/src/app/visualization/barchart/timeline.component.ts index e7aed6ffc..c7ed20611 100644 --- a/frontend/src/app/visualization/barchart/timeline.component.ts +++ b/frontend/src/app/visualization/barchart/timeline.component.ts @@ -269,11 +269,11 @@ export class TimelineComponent extends BarchartDirective impl * Add a date filter to a query model restricting it to the provided min and max values. */ addQueryDateFilter(query: QueryModel, min: Date, max: Date): QueryModel { - const queryModelCopy = _.cloneDeep(query); + const queryModelCopy = query.clone(); // download zoomed in results const filter = this.visualizedField.makeSearchFilter(); filter.data.next({ min, max }); - queryModelCopy.filters.push(filter); + queryModelCopy.addFilter(filter); return queryModelCopy; } From 1c2367e0fe09e43655d65ddfcd8d903bc1669226 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:43:44 +0100 Subject: [PATCH 093/262] fix corpusfield reference in donwload component --- frontend/src/app/download/download.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/download/download.component.ts b/frontend/src/app/download/download.component.ts index 5775eaf05..c79182a66 100644 --- a/frontend/src/app/download/download.component.ts +++ b/frontend/src/app/download/download.component.ts @@ -38,7 +38,7 @@ export class DownloadComponent implements OnChanges { ngOnChanges() { this.availableCsvFields = Object.values(this.corpus.fields).filter(field => field.downloadable); - const highlight = this.queryModel.highlight; + const highlight = this.queryModel.highlightSize; // 'Query in context' becomes an extra option if any field in the corpus has been marked as highlightable if (highlight !== undefined) { this.availableCsvFields.push({ @@ -52,9 +52,9 @@ export class DownloadComponent implements OnChanges { primarySort: false, searchable: false, downloadable: true, - searchFilter: null, + filterOptions: null, mappingType: null, - }); + } as unknown as CorpusField) ; } } From 4c5367466276f9b126f1e16e96646d400f3d6366 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 19:10:49 +0100 Subject: [PATCH 094/262] update adhoc filter template --- frontend/src/app/filter/ad-hoc-filter.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/filter/ad-hoc-filter.component.html b/frontend/src/app/filter/ad-hoc-filter.component.html index 25048bd00..ecab52e8a 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.html +++ b/frontend/src/app/filter/ad-hoc-filter.component.html @@ -1 +1 @@ -

{{data.value}}

+

{{data}}

From 0d40dee174c836d35725e22349d9a24da07ad8c7 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 19:04:45 +0100 Subject: [PATCH 095/262] update search sorting component --- frontend/src/app/search/search-sorting.component.spec.ts | 5 +++-- frontend/src/app/search/search.component.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/search/search-sorting.component.spec.ts b/frontend/src/app/search/search-sorting.component.spec.ts index 5be0bc405..ae1ebdd95 100644 --- a/frontend/src/app/search/search-sorting.component.spec.ts +++ b/frontend/src/app/search/search-sorting.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { mockField3 } from '../../mock-data/corpus'; +import { mockCorpus } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; +import { QueryModel } from '../models'; import { SearchSortingComponent } from './search-sorting.component'; @@ -16,7 +17,7 @@ describe('Search Sorting Component', () => { beforeEach(() => { fixture = TestBed.createComponent(SearchSortingComponent); component = fixture.componentInstance; - component.fields = [mockField3]; + component.queryModel = new QueryModel(mockCorpus); fixture.detectChanges(); }); diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 5432bb63e..30665cb88 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -72,7 +72,7 @@
- + From 2038d207506c9f92d787f743549f6a076a1b812b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 18:15:37 +0100 Subject: [PATCH 096/262] fixes to search history based on query model updates --- .../search-history/query-filters.component.spec.ts | 8 ++++---- .../search-history/query-filters.component.ts | 11 +++++------ .../search-history/search-history.component.ts | 12 +++++------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/history/search-history/query-filters.component.spec.ts b/frontend/src/app/history/search-history/query-filters.component.spec.ts index 0886cde6c..5acccd124 100644 --- a/frontend/src/app/history/search-history/query-filters.component.spec.ts +++ b/frontend/src/app/history/search-history/query-filters.component.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { QueryModel } from '../../models'; +import { mockCorpus } from '../../../mock-data/corpus'; import { commonTestBed } from '../../common-test-bed'; import { QueryFiltersComponent } from './query-filters.component'; @@ -14,10 +16,8 @@ describe('QueryFiltersComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(QueryFiltersComponent); component = fixture.componentInstance; - component.queryModel = { - queryText: 'testing', - filters: [] - }; + component.queryModel = new QueryModel(mockCorpus); + component.queryModel.setQueryText('testing'); fixture.detectChanges(); }); diff --git a/frontend/src/app/history/search-history/query-filters.component.ts b/frontend/src/app/history/search-history/query-filters.component.ts index 475cf5fef..a3e5406e5 100644 --- a/frontend/src/app/history/search-history/query-filters.component.ts +++ b/frontend/src/app/history/search-history/query-filters.component.ts @@ -1,8 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ParamService } from '../../services'; import { QueryModel } from '../../models/index'; -import { searchFilterDataToParam } from '../../utils/params'; @Component({ selector: '[ia-query-filters]', @@ -15,13 +13,14 @@ export class QueryFiltersComponent implements OnInit { name: string; formattedData: string | string[]; }[]; - constructor(private paramService: ParamService) { } + constructor() { } ngOnInit() { if (this.queryModel.filters?.length>0) { - this.formattedFilters = this.queryModel.filters.map(filter => { - return {name: filter.fieldName, formattedData: searchFilterDataToParam(filter)} - }); + this.formattedFilters = this.queryModel.filters.map(filter => ({ + name: filter.corpusField.name, + formattedData: filter.dataToString(filter.currentData) + })); } } diff --git a/frontend/src/app/history/search-history/search-history.component.ts b/frontend/src/app/history/search-history/search-history.component.ts index 01f413c3c..8d3a4663c 100644 --- a/frontend/src/app/history/search-history/search-history.component.ts +++ b/frontend/src/app/history/search-history/search-history.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import * as _ from 'lodash'; +import { esQueryToQueryModel } from '../../utils/es-query'; import { QueryDb } from '../../models/index'; -import { CorpusService, SearchService, QueryService, ParamService, ElasticSearchService } from '../../services/index'; +import { CorpusService, QueryService } from '../../services/index'; import { HistoryDirective } from '../history.directive'; @Component({ @@ -14,11 +15,9 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { public queries: QueryDb[]; public displayCorpora = false; constructor( - private paramService: ParamService, corpusService: CorpusService, private queryService: QueryService, private router: Router, - private elasticSearchService: ElasticSearchService, ) { super(corpusService); } @@ -35,16 +34,15 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { addQueryModel(query?: QueryDb) { const corpus = this.corpora.find(c => c.name === query.corpus); - query.queryModel = this.elasticSearchService.esQueryToQueryModel(query.query_json, corpus); + query.queryModel = esQueryToQueryModel(query.query_json, corpus); return query; } returnToSavedQuery(query: QueryDb) { - const route = this.paramService.queryModelToRoute(query.queryModel); - this.router.navigate(['/search', query.corpus, route]); + const params = query.queryModel.toRouteParam(); + this.router.navigate(['/search', query.corpus, params]); if (window) { window.scrollTo(0, 0); } } - } From 5bd44496f7559e07be41afe0403158487473c51a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 19:02:12 +0100 Subject: [PATCH 097/262] clean up filter manager component --- frontend/src/app/filter/base-filter.component.ts | 2 -- frontend/src/app/filter/filter-manager.component.spec.ts | 6 ++---- frontend/src/app/filter/filter-manager.component.ts | 4 ---- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 7c79c55b1..0e8f91d2b 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -11,8 +11,6 @@ import { PotentialFilter, SearchFilter } from '../models/index'; template: '' }) export abstract class BaseFilterComponent implements OnChanges { - @Input() inputChanged: Subject; - @Input() public filter: PotentialFilter; diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index d234c6b66..7f0e5c0fc 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -3,9 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { commonTestBed } from '../common-test-bed'; import { FilterManagerComponent } from './filter-manager.component'; -import { mockCorpus, mockCorpus2, mockFilter } from '../../mock-data/corpus'; -import { convertToParamMap } from '@angular/router'; -import { findByName } from '../utils/utils'; +import { mockCorpus, mockCorpus2 } from '../../mock-data/corpus'; import { QueryModel } from '../models'; describe('FilterManagerComponent', () => { @@ -40,7 +38,7 @@ describe('FilterManagerComponent', () => { expect(component.potentialFilters.length).toEqual(1); }); - it('toggles filters on and off', async() => { + it('toggles filters on and off', async () => { const filter = component.potentialFilters.find(f => f.corpusField.name === 'great_field'); expect(component.queryModel.filters.length).toBe(0); component.toggleFilter(filter); diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 7d2a31df1..3f51329bd 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -2,7 +2,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import * as _ from 'lodash'; -import { Subject } from 'rxjs'; import { PotentialFilter, Corpus, SearchFilter, QueryModel, MultipleChoiceFilterOptions, AggregateData } from '../models/index'; import { SearchService } from '../services'; @@ -16,8 +15,6 @@ export class FilterManagerComponent implements OnChanges { @Input() public corpus: Corpus; @Input() queryModel: QueryModel; - inputChanged = new Subject(); - public potentialFilters: PotentialFilter[] = []; public showFilters: boolean; @@ -40,7 +37,6 @@ export class FilterManagerComponent implements OnChanges { this.potentialFilters = this.corpus.fields.map(field => new PotentialFilter(field, this.queryModel)); this.queryModel.update.subscribe(this.onQueryModelUpdate.bind(this)); } - this.inputChanged.next(); } onQueryModelUpdate() { From 020d297b9998557105f9d7c8ad63ddede9b71bae Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 14:05:59 +0100 Subject: [PATCH 098/262] more tests for search filters --- frontend/src/app/models/search-filter.spec.ts | 71 ++++++++++++++++--- frontend/src/app/models/search-filter.ts | 5 +- frontend/src/mock-data/corpus.ts | 26 +++++++ 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index 48ba595ff..1d04ec8a8 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -1,11 +1,17 @@ -import { mockFieldDate } from '../../mock-data/corpus'; -import { DateFilter } from './search-filter'; +import { mockFieldMultipleChoice, mockFieldDate } from '../../mock-data/corpus'; +import { DateFilter, MultipleChoiceFilter } from './search-filter'; describe('DateFilter', () => { + const field = mockFieldDate; + let filter: DateFilter; + + beforeEach(() => { + filter = new DateFilter(field); + }); it('should create', () => { - const filter = new DateFilter(mockFieldDate); expect(filter).toBeTruthy(); + expect(filter.currentData).toEqual(filter.defaultData); expect(filter.currentData).toEqual({ min: new Date(Date.parse('Jan 01 1800')), max: new Date(Date.parse('Dec 31 1899')) @@ -13,13 +19,11 @@ describe('DateFilter', () => { }); it('should convert to string', () => { - const filter = new DateFilter(mockFieldDate); - const dataAsString = filter.dataToString(filter.currentData); - expect(filter.dataFromString(dataAsString)).toEqual(filter.currentData); + expect(filter.dataFromString(filter.dataToString(filter.currentData))) + .toEqual(filter.currentData); }); it('should set data from a value', () => { - const filter = new DateFilter(mockFieldDate); const date = new Date(Date.parse('Jan 01 1850')); filter.setToValue(date); expect(filter.currentData).toEqual({ @@ -29,7 +33,6 @@ describe('DateFilter', () => { }); it('should convert to an elasticsearch filter', () => { - const filter = new DateFilter(mockFieldDate); const esFilter = filter.toEsFilter(); expect(esFilter).toEqual({ range: { @@ -41,4 +44,56 @@ describe('DateFilter', () => { } }); }); + + it('should parse an elasticsearch filter', () => { + const esFilter = filter.toEsFilter(); + expect(filter.dataFromEsFilter(esFilter)).toEqual(filter.currentData); + }); +}); + +describe('MultipleChoiceFilter', () => { + const field = mockFieldMultipleChoice; + let filter: MultipleChoiceFilter; + + beforeEach(() => { + filter = new MultipleChoiceFilter(field); + }); + + it('should create', () => { + expect(filter).toBeTruthy(); + expect(filter.currentData).toEqual(filter.defaultData); + expect(filter.currentData).toEqual([]); + }); + + it('should convert to string', () => { + expect(filter.dataFromString(filter.dataToString(filter.currentData))) + .toEqual(filter.currentData); + + // non-empty value + filter.data.next(['a', 'b']); + expect(filter.dataFromString(filter.dataToString(filter.currentData))) + .toEqual(filter.currentData); + + }); + + it('should set data from a value', () => { + const value = 'a great value'; + filter.setToValue(value); + expect(filter.currentData).toEqual([value]); + }); + + it('should convert to an elasticsearch filter', () => { + filter.data.next(['wow!']); + const esFilter = filter.toEsFilter(); + expect(esFilter).toEqual({ + terms: { + greater_field: ['wow!'] + } + }); + }); + + it('should parse an elasticsearch filter', () => { + const esFilter = filter.toEsFilter(); + expect(filter.dataFromEsFilter(esFilter)).toEqual(filter.currentData); + }); }); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 49f7273c1..003a83545 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -168,7 +168,10 @@ export class MultipleChoiceFilter extends AbstractSearchFilter Date: Wed, 15 Mar 2023 16:00:45 +0100 Subject: [PATCH 099/262] fix names in makesearchfilter --- frontend/src/app/models/corpus.ts | 12 ++++++------ frontend/src/mock-data/corpus.ts | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/models/corpus.ts b/frontend/src/app/models/corpus.ts index d7d5acfad..423792f7c 100644 --- a/frontend/src/app/models/corpus.ts +++ b/frontend/src/app/models/corpus.ts @@ -89,7 +89,7 @@ export class CorpusField { searchable: boolean; downloadable: boolean; name: string; - filterOptions: FilterOptions | null; + filterOptions: FilterOptions; mappingType: 'text' | 'keyword' | 'boolean' | 'date' | 'integer' | null; constructor(data: ApiCorpusField) { @@ -110,17 +110,17 @@ export class CorpusField { this.searchable = data.searchable; this.downloadable = data.downloadable; this.name = data.name; - this.filterOptions = data['search_filter'] || null; + this.filterOptions = data['search_filter']; this.mappingType = data.es_mapping?.type; } /** make a SearchFilter for this field */ makeSearchFilter(): SearchFilter { const filterClasses = { - date: DateFilter, - multiple_choice: MultipleChoiceFilter, - boolean: BooleanFilter, - range: RangeFilter, + DateFilter, + MultipleChoiceFilter, + BooleanFilter, + RangeFilter, }; const Filter = _.get( filterClasses, diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index e0c3b850b..37c4417c6 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -93,7 +93,12 @@ export const mockField3 = new CorpusField({ searchable: false, downloadable: true, results_overview: true, - search_filter: null, + search_filter: { + name: 'RangeFilter', + description: 'Filter me', + lower: 0, + upper: 100, + }, search_field_core: false, csv_core: true, visualizations: [], From 229110af6296ff3430406b362ee37be22e8f26e7 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 15:37:31 +0100 Subject: [PATCH 100/262] draft new structure for filter widgets --- .../app/filter/ad-hoc-filter.component.html | 2 +- .../src/app/filter/ad-hoc-filter.component.ts | 13 +++-- .../src/app/filter/base-filter.component.ts | 55 ++++++++++--------- .../app/filter/boolean-filter.component.html | 4 +- .../app/filter/boolean-filter.component.ts | 13 +++-- .../src/app/filter/date-filter.component.html | 10 ++-- .../app/filter/date-filter.component.spec.ts | 4 +- .../src/app/filter/date-filter.component.ts | 21 +++---- .../app/filter/filter-manager.component.html | 9 ++- .../multiple-choice-filter.component.html | 7 ++- .../multiple-choice-filter.component.spec.ts | 8 +-- .../multiple-choice-filter.component.ts | 39 +++++-------- .../app/filter/range-filter.component.html | 6 +- .../app/filter/range-filter.component.spec.ts | 2 +- .../src/app/filter/range-filter.component.ts | 22 ++++---- 15 files changed, 103 insertions(+), 112 deletions(-) diff --git a/frontend/src/app/filter/ad-hoc-filter.component.html b/frontend/src/app/filter/ad-hoc-filter.component.html index ecab52e8a..9632a8dfd 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.html +++ b/frontend/src/app/filter/ad-hoc-filter.component.html @@ -1 +1 @@ -

{{data}}

+

{{data$ | async}}

diff --git a/frontend/src/app/filter/ad-hoc-filter.component.ts b/frontend/src/app/filter/ad-hoc-filter.component.ts index 27b9c1e1d..ab260a115 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.ts +++ b/frontend/src/app/filter/ad-hoc-filter.component.ts @@ -7,15 +7,16 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './ad-hoc-filter.component.html', styleUrls: ['./ad-hoc-filter.component.scss'] }) -export class AdHocFilterComponent extends BaseFilterComponent { +export class AdHocFilterComponent extends BaseFilterComponent { data: any; - getDisplayData(filter: AdHocFilter) { - return filter.currentData; - } + onFilterSet() {} - getFilterData(): any { - return this.data; + getDisplayData(filterData: any) { + return filterData; } + getFilterData(data): any { + return data; + } } diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 0e8f91d2b..4bd7f274f 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -1,5 +1,6 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { Subject } from 'rxjs'; +import { Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { PotentialFilter, SearchFilter } from '../models/index'; @@ -10,9 +11,20 @@ import { PotentialFilter, SearchFilter } from '../models/index'; @Component({ template: '' }) -export abstract class BaseFilterComponent implements OnChanges { +export abstract class BaseFilterComponent { + private _filter: PotentialFilter; + @Input() - public filter: PotentialFilter; + get filter() { + return this._filter; + } + set filter(filter: PotentialFilter) { + this._filter = filter; + this.data$ = this.filter.filter.data.asObservable().pipe( + map(this.getDisplayData.bind(this)) + ); + this.onFilterSet(filter.filter as SearchFilterClass); + } @Input() public grayedOut: boolean; @@ -20,42 +32,31 @@ export abstract class BaseFilterComponent; // - constructor() { - } + constructor() { } - ngOnChanges(changes): void { - if (changes.filter) { - this.filter.filter.data.subscribe(this.provideFilterData.bind(this)); - } - } - - provideFilterData() { - if (this.filter) { - this.data = this.getDisplayData(this.filter.filter as SearchFilterClass); - } - } /** * Trigger a change event. */ - update() { - const data = this.getFilterData(); - this.filter.filter.data.next(data); - if (this.data.selected && this.data.selected.length === 0) { + update(data: Data) { + const filterData = this.getFilterData(data); + this.filter.filter.data.next(filterData); + if ((data as any).selected && (data as any).selected.length === 0) { this.filter.deactivate(); } else { this.filter.activate(); // update called through user input } } + /** possible administration when the filter is set, e.g. setting data limits */ + abstract onFilterSet(filter: SearchFilterClass): void; - abstract getDisplayData(filter: SearchFilterClass); + /** get the internal data representation from the filter data */ + abstract getDisplayData(filterData): Data; - /** - * Create a new version of the filter data from the user input. - */ - abstract getFilterData(): typeof this.filter.filter.currentData; + /** get the filter data formt from the internal representation */ + abstract getFilterData(data: Data); } diff --git a/frontend/src/app/filter/boolean-filter.component.html b/frontend/src/app/filter/boolean-filter.component.html index 62a871302..a42a821d3 100644 --- a/frontend/src/app/filter/boolean-filter.component.html +++ b/frontend/src/app/filter/boolean-filter.component.html @@ -1,4 +1,4 @@ -
- +
+ {{data | titlecase }}
diff --git a/frontend/src/app/filter/boolean-filter.component.ts b/frontend/src/app/filter/boolean-filter.component.ts index d12a4d360..f51c48aeb 100644 --- a/frontend/src/app/filter/boolean-filter.component.ts +++ b/frontend/src/app/filter/boolean-filter.component.ts @@ -8,16 +8,17 @@ import { BooleanFilter } from '../models'; templateUrl: './boolean-filter.component.html', styleUrls: ['./boolean-filter.component.scss'] }) -export class BooleanFilterComponent extends BaseFilterComponent { +export class BooleanFilterComponent extends BaseFilterComponent { data: boolean; - getDisplayData(filter: BooleanFilter) { - const data = filter.currentData; - return data; + onFilterSet(filter: BooleanFilter) {} + + getDisplayData(filterData: boolean): boolean { + return filterData; } - getFilterData(): boolean { - return this.data; + getFilterData(data: boolean): boolean { + return data; } } diff --git a/frontend/src/app/filter/date-filter.component.html b/frontend/src/app/filter/date-filter.component.html index 34feec72d..8104472ff 100644 --- a/frontend/src/app/filter/date-filter.component.html +++ b/frontend/src/app/filter/date-filter.component.html @@ -1,8 +1,8 @@ -
- + - + + (onSelect)="update($event)" (onClose)="update($event.target.value)" dateFormat="dd-mm-yy">
diff --git a/frontend/src/app/filter/date-filter.component.spec.ts b/frontend/src/app/filter/date-filter.component.spec.ts index 1b3efbf47..b0364368e 100644 --- a/frontend/src/app/filter/date-filter.component.spec.ts +++ b/frontend/src/app/filter/date-filter.component.spec.ts @@ -19,10 +19,10 @@ describe('DateFilterComponent', () => { component = fixture.componentInstance; const queryModel = new QueryModel(mockCorpus3); component.filter = new PotentialFilter(mockFieldDate, queryModel); - component.data = { + component.filter.filter.data.next({ min: new Date('Jan 1 1810'), max: new Date('Dec 31 1820') - }; + }); fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/date-filter.component.ts b/frontend/src/app/filter/date-filter.component.ts index 70060aa36..bcca134c7 100644 --- a/frontend/src/app/filter/date-filter.component.ts +++ b/frontend/src/app/filter/date-filter.component.ts @@ -10,32 +10,27 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './date-filter.component.html', styleUrls: ['./date-filter.component.scss'] }) -export class DateFilterComponent extends BaseFilterComponent implements OnInit { +export class DateFilterComponent extends BaseFilterComponent { public minDate: Date; public maxDate: Date; public minYear: number; public maxYear: number; - ngOnInit() { - this.provideFilterData(); - this.minDate = this.filter.filter.defaultData.min; - this.maxDate = this.filter.filter.defaultData.max; + onFilterSet(filter: DateFilter): void { + this.minDate = filter.defaultData.min; + this.maxDate = filter.defaultData.max; this.minYear = this.minDate.getFullYear(); this.maxYear = this.maxDate.getFullYear(); } - getDisplayData(filter: DateFilter) { - const data = filter.currentData; - return { - min: new Date(data.min), - max: new Date(data.max), - }; + getDisplayData(filterData: DateFilterData) { + return filterData; } /** * Create a new version of the filter data from the user input. */ - getFilterData(): DateFilterData { - return this.data; + getFilterData(data): DateFilterData { + return data; } } diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index 0ee76513e..396594835 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -44,11 +44,10 @@

Filters

- - - - + + + +
diff --git a/frontend/src/app/filter/multiple-choice-filter.component.html b/frontend/src/app/filter/multiple-choice-filter.component.html index a2986c768..7f06605b1 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.html +++ b/frontend/src/app/filter/multiple-choice-filter.component.html @@ -1,8 +1,9 @@ -
- +
+
{{item.label}}
-
{{item.doc_count}}
+
{{item.doc_count}}
diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index b9e8a11de..6da214b6c 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { mockCorpus, mockFieldMultipleChoice } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; +import { PotentialFilter, QueryModel } from '../models'; import { MultipleChoiceFilterComponent } from './multiple-choice-filter.component'; @@ -16,10 +18,8 @@ describe('MultipleChoiceFilterComponent', () => { fixture = TestBed.createComponent(MultipleChoiceFilterComponent); component = fixture.componentInstance; component.optionsAndCounts = [{key: 'Andy', doc_count: 2}, {key: 'Lou', doc_count: 3}]; - component.data = { - options: ['Andy', 'Lou'], - selected: [], - }; + const query = new QueryModel(mockCorpus); + component.filter = new PotentialFilter(mockFieldMultipleChoice, query); fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index 53dbaaf24..3ce46f04e 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -10,33 +10,24 @@ import { SearchFilter, AggregateResult, PotentialFilter, MultipleChoiceFilter } templateUrl: './multiple-choice-filter.component.html', styleUrls: ['./multiple-choice-filter.component.scss'] }) -export class MultipleChoiceFilterComponent extends BaseFilterComponent { - @Input() potentialFilter: PotentialFilter; - @Input() public optionsAndCounts: AggregateResult[]; - - data: { - options: string[]; - selected: string[]; - } = { - options: [], selected: [] +export class MultipleChoiceFilterComponent extends BaseFilterComponent { + options: { label: string; value: string; doc_count: number }[]; + + @Input() + set optionsAndCounts(value: AggregateResult[]) { + this.options = _.sortBy( + value.map(x => ({ label: x.key, value: encodeURIComponent(x.key), doc_count: x.doc_count })), + o => o.label + ); }; - getDisplayData(filter: MultipleChoiceFilter) { - let options = []; - if (this.optionsAndCounts) { - options = _.sortBy( - this.optionsAndCounts.map(x => ({ label: x.key, value: encodeURIComponent(x.key), doc_count: x.doc_count })), - o => o.label - ); - } else { - options = [1, 2, 3]; - } // dummy array to make sure the component loads - return { options, selected: filter.currentData }; - } + onFilterSet(filter: MultipleChoiceFilter): void {} - getFilterData(): string[] { - return this.data.selected; + getDisplayData(filterData: string[]): string[] { + return filterData; } - + getFilterData(data: string[]): string[] { + return data; + } } diff --git a/frontend/src/app/filter/range-filter.component.html b/frontend/src/app/filter/range-filter.component.html index 9f82ac618..e97fdb198 100644 --- a/frontend/src/app/filter/range-filter.component.html +++ b/frontend/src/app/filter/range-filter.component.html @@ -1,4 +1,4 @@ -
+
{{data[0]}} - {{data[1]}} - -
\ No newline at end of file + +
diff --git a/frontend/src/app/filter/range-filter.component.spec.ts b/frontend/src/app/filter/range-filter.component.spec.ts index cfaeef80e..e82968366 100644 --- a/frontend/src/app/filter/range-filter.component.spec.ts +++ b/frontend/src/app/filter/range-filter.component.spec.ts @@ -19,7 +19,7 @@ describe('RangeFilterComponent', () => { component = fixture.componentInstance; const query = new QueryModel(mockCorpus3); component.filter = new PotentialFilter(mockField3, query); - component.data = [1984, 1984]; + component.filter.filter.data.next({min: 1984, max: 1984}); fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/range-filter.component.ts b/frontend/src/app/filter/range-filter.component.ts index 290dee09b..eecba78c0 100644 --- a/frontend/src/app/filter/range-filter.component.ts +++ b/frontend/src/app/filter/range-filter.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { SearchFilter, RangeFilterData, RangeFilter } from '../models'; +import { RangeFilterData, RangeFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @Component({ @@ -8,21 +8,23 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './range-filter.component.html', styleUrls: ['./range-filter.component.scss'] }) -export class RangeFilterComponent extends BaseFilterComponent implements OnInit { - data: [number, number]; +export class RangeFilterComponent extends BaseFilterComponent<[number, number], RangeFilter> { + min: number; + max: number; - ngOnInit() { - this.provideFilterData(); + onFilterSet(filter: RangeFilter): void { + this.min = filter.defaultData.min; + this.max = filter.defaultData.max; } - getDisplayData(filter: RangeFilter) { - return [filter.currentData.min, filter.currentData.max]; + getDisplayData(filterData: RangeFilterData): [number, number] { + return [filterData.min, filterData.max]; } - getFilterData(): RangeFilterData { + getFilterData(value: [number, number]): RangeFilterData { return { - min: this.data[0], - max: this.data[1], + min: value[0], + max: value[1], }; } From 23c0f936393c7e176b43e893c3284a05f5711bbb Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 16:05:07 +0100 Subject: [PATCH 101/262] remove obsolete functions from filter manager --- frontend/src/app/filter/filter-manager.component.spec.ts | 2 +- frontend/src/app/filter/filter-manager.component.ts | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index 7f0e5c0fc..5fa5de9cd 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -41,7 +41,7 @@ describe('FilterManagerComponent', () => { it('toggles filters on and off', async () => { const filter = component.potentialFilters.find(f => f.corpusField.name === 'great_field'); expect(component.queryModel.filters.length).toBe(0); - component.toggleFilter(filter); + filter.toggle(); expect(component.queryModel.filters.length).toBe(1); }); }); diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 3f51329bd..67379bb03 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -77,13 +77,6 @@ export class FilterManagerComponent implements OnChanges { response => response.aggregations); } - toggleFilter(filter: PotentialFilter) { - filter.toggle(); - } - - resetFilter(filter: PotentialFilter) { - filter.reset(); - } public toggleActiveFilters() { if (this.activeFilters.length) { From 867925e8167ee7becd28bca2f56545cdd8c42876 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 16:19:08 +0100 Subject: [PATCH 102/262] add query model to search results spec --- .../src/app/search/search-results.component.spec.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/search/search-results.component.spec.ts b/frontend/src/app/search/search-results.component.spec.ts index 0dcfda93e..d66457861 100644 --- a/frontend/src/app/search/search-results.component.spec.ts +++ b/frontend/src/app/search/search-results.component.spec.ts @@ -1,9 +1,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import * as _ from 'lodash'; -import { mockField } from '../../mock-data/corpus'; +import { mockCorpus, mockField } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; -import { CorpusField } from '../models/index'; +import { CorpusField, QueryModel } from '../models/index'; import { SearchResultsComponent } from './search-results.component'; @@ -40,11 +40,13 @@ describe('Search Results Component', () => { relation: 'gte' } }; - component.corpus = { - fields - } as any; + component.corpus = _.merge(mockCorpus, fields); component.fromIndex = 0; component.resultsPerPage = 20; + const query = new QueryModel(component.corpus); + query.setQueryText('wally'); + query.setHighlight(10); + component.queryModel = query; fixture.detectChanges(); }); From 71fdd75f472d72c4fc4423b2ee2536f86cffe69d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 14 Mar 2023 19:01:02 +0100 Subject: [PATCH 103/262] fix document view component based on querymodel updates --- .../document-view.component.spec.ts | 26 +++++++------------ .../document-view/document-view.component.ts | 7 ++--- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/document-view/document-view.component.spec.ts b/frontend/src/app/document-view/document-view.component.spec.ts index cd43d313b..5e7d9e458 100644 --- a/frontend/src/app/document-view/document-view.component.spec.ts +++ b/frontend/src/app/document-view/document-view.component.spec.ts @@ -1,5 +1,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import * as _ from 'lodash'; +import { mockCorpus, mockField } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; @@ -16,26 +18,14 @@ describe('DocumentViewComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DocumentViewComponent); component = fixture.componentInstance; - component.corpus = { - scan_image_type: 'farout_image_type' - }; - component.fields = [{ - name: 'test', - displayName: 'Test', - displayType: 'text', - description: 'Description', - hidden: false, - sortable: false, - primarySort: false, - searchable: false, - searchFilter: null, - downloadable: true, - mappingType: 'text' - }]; + component.corpus = _.merge({ + scan_image_type: 'farout_image_type', + fields: [mockField] + }, mockCorpus); component.document = { id: 'test', relevance: 0.5, - fieldValues: { test: 'Hello world!' } + fieldValues: { great_field: 'Hello world!' } }; fixture.detectChanges(); }); @@ -47,6 +37,8 @@ describe('DocumentViewComponent', () => { it('should render fields', async () => { await fixture.whenStable(); + expect(component.propertyFields).toEqual([mockField]); + const debug = fixture.debugElement.queryAll(By.css('[data-test-field-value]')); expect(debug.length).toEqual(1); // number of fields const element = debug[0].nativeElement; diff --git a/frontend/src/app/document-view/document-view.component.ts b/frontend/src/app/document-view/document-view.component.ts index 05d16fb53..6018e8cac 100644 --- a/frontend/src/app/document-view/document-view.component.ts +++ b/frontend/src/app/document-view/document-view.component.ts @@ -11,16 +11,13 @@ import { CorpusField, FoundDocument, Corpus } from '../models/index'; export class DocumentViewComponent implements OnChanges { public get contentFields() { - return this.fields.filter(field => !field.hidden && field.displayType === 'text_content'); + return this.corpus.fields.filter(field => !field.hidden && field.displayType === 'text_content'); } public get propertyFields() { - return this.fields.filter(field => !field.hidden && field.displayType !== 'text_content'); + return this.corpus.fields.filter(field => !field.hidden && field.displayType !== 'text_content'); } - @Input() - public fields: CorpusField[] = []; - @Input() public document: FoundDocument; From faf987c3efef66771bdcf621f280c3e663f791f3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 17:13:31 +0100 Subject: [PATCH 104/262] more fixed to filter manager --- .../app/filter/filter-manager.component.html | 4 +-- .../filter/filter-manager.component.spec.ts | 8 +++-- .../app/filter/filter-manager.component.ts | 33 ++++++++++++++----- frontend/src/app/models/filter-management.ts | 2 ++ 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index 396594835..52d1d6de1 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -17,7 +17,7 @@

Filters

- +
Filters

}}

-

From 87921218326d6bb3cef2378a3da9c72eca34e6da Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 17:54:26 +0100 Subject: [PATCH 107/262] update search results component --- frontend/src/app/search/search-results.component.html | 2 +- frontend/src/app/search/search-results.component.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/search/search-results.component.html b/frontend/src/app/search/search-results.component.html index 596cec682..3db19c305 100644 --- a/frontend/src/app/search/search-results.component.html +++ b/frontend/src/app/search/search-results.component.html @@ -80,7 +80,7 @@

- +
diff --git a/frontend/src/app/search/search-results.component.ts b/frontend/src/app/search/search-results.component.ts index 52cd8bea4..01e7fe21b 100644 --- a/frontend/src/app/search/search-results.component.ts +++ b/frontend/src/app/search/search-results.component.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/member-ordering */ import { Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output, ViewChild } from '@angular/core'; import { User, Corpus, SearchParameters, SearchResults, FoundDocument, QueryModel, ResultOverview } from '../models/index'; @@ -75,9 +76,10 @@ export class SearchResultsComponent implements OnChanges { constructor(private searchService: SearchService) { } ngOnChanges() { - if (this.queryModel !== null) { + if (this.queryModel) { this.fromIndex = 0; this.maximumDisplayed = this.user.downloadLimit ? this.user.downloadLimit : 10000; + this.search(); this.queryModel.update.subscribe(() => this.search()); } } From 4768aaeccc5aac58351089adf4463ad647dec473 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 18:20:42 +0100 Subject: [PATCH 108/262] retrieve multiple choice options within component --- .../app/filter/filter-manager.component.html | 4 +- .../app/filter/filter-manager.component.ts | 45 +------------------ .../multiple-choice-filter.component.html | 2 +- .../multiple-choice-filter.component.ts | 34 +++++++++----- 4 files changed, 29 insertions(+), 56 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index 52d1d6de1..efbef00bd 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -10,7 +10,7 @@

Filters

@@ -46,7 +46,7 @@

Filters

- +
diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 3275c14b3..ca46c73c8 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -3,8 +3,7 @@ import { Component, Input } from '@angular/core'; import * as _ from 'lodash'; -import { PotentialFilter, Corpus, SearchFilter, QueryModel, MultipleChoiceFilterOptions, AggregateData } from '../models/index'; -import { SearchService } from '../services'; +import { PotentialFilter, Corpus, SearchFilter, QueryModel} from '../models/index'; @Component({ selector: 'ia-filter-manager', @@ -28,7 +27,6 @@ export class FilterManagerComponent { set queryModel(model: QueryModel) { this._queryModel = model; this.setPotentialFilters(); - model.update.subscribe(this.onQueryModelUpdate.bind(this)); } public potentialFilters: PotentialFilter[] = []; @@ -43,8 +41,7 @@ export class FilterManagerComponent { private _corpus: Corpus; private _queryModel: QueryModel; - constructor( - private searchService: SearchService,) { + constructor() { } get activeFilters(): SearchFilter[] { @@ -57,44 +54,6 @@ export class FilterManagerComponent { } } - onQueryModelUpdate() { - this.aggregateSearchForMultipleChoiceFilters(); - } - - /** - * For all multiple choice filters, get the bins and counts - * Exclude the filter itself from the aggregate search - * Save results in multipleChoiceData, which is structured as follows: - * fieldName1: [{key: option1, doc_count: 42}, {key: option2, doc_count: 3}], - * fieldName2: [etc] - */ - private aggregateSearchForMultipleChoiceFilters() { - const multipleChoiceFilters = this.potentialFilters.filter(f => - f.corpusField.filterOptions?.name === 'MultipleChoiceFilter'); - - const aggregateResultPromises = multipleChoiceFilters.map(filter => - this.getMultipleChoiceFilterOptions(filter)); - Promise.all(aggregateResultPromises).then(results => { - results.forEach( r => - this.multipleChoiceData[Object.keys(r)[0]] = Object.values(r)[0] - ); - // if multipleChoiceData is empty, gray out all filters - if (multipleChoiceFilters && multipleChoiceFilters.length !== 0) { - this.grayOutFilters = this.multipleChoiceData[multipleChoiceFilters[0].corpusField.name].length === 0; - } - }); - } - - private async getMultipleChoiceFilterOptions(filter: PotentialFilter): Promise { - const optionCount = (filter.corpusField.filterOptions as MultipleChoiceFilterOptions).option_count; - const aggregator = {name: filter.corpusField.name, size: optionCount}; - const queryModel = this.queryModel.clone(); - queryModel.removeFilter(filter.filter); // exclude the choices for this filter - return this.searchService.aggregateSearch(this.corpus, queryModel, [aggregator]).then( - response => response.aggregations); - } - - public toggleActiveFilters() { if (this.activeFilters.length) { this.potentialFilters.forEach(filter => filter.deactivate()); diff --git a/frontend/src/app/filter/multiple-choice-filter.component.html b/frontend/src/app/filter/multiple-choice-filter.component.html index 7f06605b1..4307ec0ac 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.html +++ b/frontend/src/app/filter/multiple-choice-filter.component.html @@ -1,6 +1,6 @@
+ placeholder="Choose" [ngModel]="data" (onChange)="update($event.value)">
{{item.label}}
{{item.doc_count}}
diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index 3ce46f04e..4687d43a1 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -3,7 +3,8 @@ import { Component, Input, OnInit, OnChanges } from '@angular/core'; import * as _ from 'lodash'; import { BaseFilterComponent } from './base-filter.component'; -import { SearchFilter, AggregateResult, PotentialFilter, MultipleChoiceFilter } from '../models'; +import { SearchFilter, AggregateResult, PotentialFilter, MultipleChoiceFilter, AggregateData, QueryModel, MultipleChoiceFilterOptions } from '../models'; +import { SearchService } from '../services'; @Component({ selector: 'ia-multiple-choice-filter', @@ -11,17 +12,15 @@ import { SearchFilter, AggregateResult, PotentialFilter, MultipleChoiceFilter } styleUrls: ['./multiple-choice-filter.component.scss'] }) export class MultipleChoiceFilterComponent extends BaseFilterComponent { - options: { label: string; value: string; doc_count: number }[]; + options: { label: string; value: string; doc_count: number }[] = []; - @Input() - set optionsAndCounts(value: AggregateResult[]) { - this.options = _.sortBy( - value.map(x => ({ label: x.key, value: encodeURIComponent(x.key), doc_count: x.doc_count })), - o => o.label - ); - }; + constructor(private searchService: SearchService) { + super(); + } - onFilterSet(filter: MultipleChoiceFilter): void {} + onFilterSet(filter: MultipleChoiceFilter): void { + this.getOptions(); + } getDisplayData(filterData: string[]): string[] { return filterData; @@ -30,4 +29,19 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent { + const optionCount = (this.filter.corpusField.filterOptions as MultipleChoiceFilterOptions).option_count; + const aggregator = {name: this.filter.corpusField.name, size: optionCount}; + const queryModel = this.filter.queryModel.clone(); + queryModel.removeFilter(this.filter.filter); // exclude the choices for this filter + this.searchService.aggregateSearch(queryModel.corpus, queryModel, [aggregator]).then( + response => response.aggregations[this.filter.corpusField.name]).then(aggregations => + this.options = _.sortBy( + aggregations.map(x => ({ label: x.key, value: encodeURIComponent(x.key), doc_count: x.doc_count })), + o => o.label + ) + ); + } + } From 30c868a7f85122f3bcbb7535ef72e31d5d11ddbc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 15:07:13 +0100 Subject: [PATCH 109/262] update spec for multiple chocie filter --- .../src/app/filter/multiple-choice-filter.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index 6da214b6c..0445052ed 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -17,7 +17,7 @@ describe('MultipleChoiceFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MultipleChoiceFilterComponent); component = fixture.componentInstance; - component.optionsAndCounts = [{key: 'Andy', doc_count: 2}, {key: 'Lou', doc_count: 3}]; + component.options = [{value: 'Andy', label: 'Andy', doc_count: 2}, {value: 'Lou', label: 'Lou', doc_count: 3}]; const query = new QueryModel(mockCorpus); component.filter = new PotentialFilter(mockFieldMultipleChoice, query); fixture.detectChanges(); From e87643eeb9a9665f33a7e450b0e2674ec6f97aeb Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 16:34:03 +0100 Subject: [PATCH 110/262] fix search mock service --- frontend/src/mock-data/search.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/mock-data/search.ts b/frontend/src/mock-data/search.ts index d2cc8282a..57754e15e 100644 --- a/frontend/src/mock-data/search.ts +++ b/frontend/src/mock-data/search.ts @@ -2,11 +2,12 @@ import { SearchFilter } from '../app/models/search-filter'; import { AggregateQueryFeedback, Corpus, CorpusField, QueryModel } from '../app/models/index'; export class SearchServiceMock { - public async aggregateSearch(corpus: Corpus, queryModel: QueryModel, aggregator: string): Promise { + public async aggregateSearch(corpus: Corpus, queryModel: QueryModel, aggregator: [{name: string}]): Promise { + const name = aggregator[0].name; return { completed: false, aggregations: { - aggregator: [{ + [name]: [{ key: '1999', doc_count: 200 }, { @@ -19,6 +20,7 @@ export class SearchServiceMock { } }; } + public async getRelatedWords() {} createQueryModel( From b48224408bfdaf0e8d68751b1f1f67943f189ed3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 15 Mar 2023 18:26:26 +0100 Subject: [PATCH 111/262] set query model from params --- frontend/src/app/models/query.spec.ts | 81 ++++++++++++++++++++- frontend/src/app/models/query.ts | 18 +++-- frontend/src/app/search/search.component.ts | 1 + frontend/src/app/utils/params.ts | 32 +++++++- 4 files changed, 121 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 8954c690d..4c39e21e9 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -1,6 +1,10 @@ import { mockField2, mockFieldDate } from '../../mock-data/corpus'; +import { EsQuery } from '../services'; import { Corpus, } from './corpus'; import { QueryModel } from './query'; +import { EsSearchClause } from './elasticsearch'; +import { DateFilter } from './search-filter'; +import { convertToParamMap } from '@angular/router'; const corpus: Corpus = { name: 'mock-corpus', @@ -21,14 +25,17 @@ const corpus: Corpus = { }; describe('QueryModel', () => { + let query: QueryModel; + + beforeEach(() => { + query = new QueryModel(corpus); + }); + it('should create', () => { - const query = new QueryModel(corpus); expect(query).toBeTruthy(); }); it('should convert to an elasticsearch query', () => { - const query = new QueryModel(corpus); - expect(query.toEsQuery()).toEqual({ query: { match_all: {} @@ -37,5 +44,73 @@ describe('QueryModel', () => { query.setQueryText('test'); + expect(query.toEsQuery()).toEqual({ + query: { + simple_query_string: { + query: 'test', + lenient: true, + default_operator: 'or', + } + } + }); + }); + + it('should formulate parameters', () => { + expect(query.toRouteParam()).toEqual({ + query: null, + fields: null, + speech: null, + date: null, + sort: null, + highlight: null + }); + + query.setQueryText('test'); + + expect(query.toRouteParam()).toEqual({ + query: 'test', + fields: null, + speech: null, + date: null, + sort: null, + highlight: null, + }); + + const filter = new DateFilter(mockFieldDate); + filter.setToValue(new Date('Jan 1 1850')); + + query.addFilter(filter); + + expect(query.toRouteParam()).toEqual({ + query: 'test', + fields: null, + speech: null, + date: '1850-01-01:1850-01-01', + sort: null, + highlight: null, + }); + + query.setQueryText(''); + query.removeFilter(filter); + + expect(query.toRouteParam()).toEqual({ + query: null, + fields: null, + speech: null, + date: null, + sort: null, + highlight: null + }); + }); + + it('should set from parameters', () => { + const params = convertToParamMap({ + query: 'test', + date: '1850-01-01:1850-01-01', + }); + + query.setFromParams(params); + expect(query.queryText).toEqual('test'); + expect(query.filters.length).toBe(1); }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 8bcb38bdc..a28077c29 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -4,7 +4,7 @@ import { Subject } from 'rxjs'; import { Corpus, CorpusField, SortBy, SortDirection } from '../models/index'; import { EsQuery } from '../services'; import { combineSearchClauseAndFilters, makeEsSearchClause, makeHighlightSpecification, makeSortSpecification } from '../utils/es-query'; -import { highlightFromParams, queryFromParams, searchFieldsFromParams, sortSettingsFromParams, +import { filtersFromParams, highlightFromParams, queryFiltersToParams, queryFromParams, searchFieldsFromParams, sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; import { sortByDefault } from '../utils/sort'; import { SearchFilter } from './search-filter'; @@ -108,6 +108,11 @@ export class QueryModel { this.removeFiltersForField(filter.corpusField); } + /** get an active search filter on this query for the field (undefined if none exists) */ + filterForField(field: CorpusField): SearchFilter { + return this.filters.find(filter => filter.corpusField.name === field.name); + } + removeFiltersForField(field: CorpusField) { const filterIndex = () => this.filters.findIndex(filter => filter.corpusField.name === field.name); while (filterIndex() !== -1) { @@ -130,6 +135,7 @@ export class QueryModel { setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); + this.filters = filtersFromParams(params, this.corpus); [this.sortBy, this.sortDirection] = sortSettingsFromParams(params, this.corpus.fields); this.highlightSize = highlightFromParams(params); this.update.next(); @@ -163,13 +169,11 @@ export class QueryModel { } toRouteParam(): {[param: string]: any} { - const queryTextParams = { query: this.queryText } || {}; - const searchFieldsParams = { fields: - this.searchFields ? this.searchFields.map(f => f.name).join(',') : null - }; - const filterParams = this.filters.map(f => f.toRouteParam()); + const queryTextParams = { query: this.queryText || null }; + const searchFieldsParams = { fields: this.searchFields?.map(f => f.name).join(',') || null}; const sortParams = sortSettingsToParams(this.sortBy, this.sortDirection); - const highlightParams = this.highlightSize ? { highlight: this.highlightSize } : {}; + const highlightParams = { highlight: this.highlightSize || null }; + const filterParams = queryFiltersToParams(this); return { ...queryTextParams, diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index 8a99a310d..c42795a9a 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -133,6 +133,7 @@ export class SearchComponent extends ParamDirective { private setQueryModel() { this.queryModel = new QueryModel(this.corpus); + this.queryModel.setFromParams(this.route.snapshot.paramMap); this.queryModel.update.subscribe(() => { this.setParams(this.queryModel.toRouteParam()); }); diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index b86f0df81..65f68275b 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -1,5 +1,6 @@ import { ParamMap } from '@angular/router'; -import { Corpus, CorpusField, SortBy, SortDirection } from '../models'; +import * as _ from 'lodash'; +import { Corpus, CorpusField, QueryModel, SearchFilter, SortBy, SortDirection } from '../models'; import * as _ from 'lodash'; /** omit keys that mapp to null */ @@ -54,3 +55,32 @@ export const sortSettingsFromParams = (params: ParamMap, corpusFields: CorpusFie ]; }; + +export const filtersFromParams = (params: ParamMap, corpus: Corpus): SearchFilter[] => { + const specifiedFields = corpus.fields.filter(field => params.has(field.name)); + return specifiedFields.map(field => { + const filter = field.makeSearchFilter(); + const data = filter.dataFromString(params.get(field.name)); + filter.data.next(data); + return filter; + }); +}; + +const filterParamForField = (queryModel: QueryModel, field: CorpusField) => { + const filter = queryModel.filterForField(field); + if (filter) { + return filter.toRouteParam(); + } else { + return { [field.name]: null }; + } +}; + +export const queryFiltersToParams = (queryModel: QueryModel) => { + const filterParamsPerField = queryModel.corpus.fields.map( + field => filterParamForField(queryModel, field)); + return _.reduce( + filterParamsPerField, + _.merge, + {} + ); +}; From e99952e318fcddfa6431e364635fa097fa08443f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 17:01:16 +0100 Subject: [PATCH 112/262] fix query cloning --- frontend/src/app/models/query.spec.ts | 19 ++++++++++++++++--- frontend/src/app/models/query.ts | 6 ++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 4c39e21e9..1b073644c 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -1,8 +1,6 @@ import { mockField2, mockFieldDate } from '../../mock-data/corpus'; -import { EsQuery } from '../services'; import { Corpus, } from './corpus'; import { QueryModel } from './query'; -import { EsSearchClause } from './elasticsearch'; import { DateFilter } from './search-filter'; import { convertToParamMap } from '@angular/router'; @@ -78,7 +76,6 @@ describe('QueryModel', () => { const filter = new DateFilter(mockFieldDate); filter.setToValue(new Date('Jan 1 1850')); - query.addFilter(filter); expect(query.toRouteParam()).toEqual({ @@ -113,4 +110,20 @@ describe('QueryModel', () => { expect(query.queryText).toEqual('test'); expect(query.filters.length).toBe(1); }); + + it('should clone', () => { + query.setQueryText('test'); + const filter = new DateFilter(mockFieldDate); + filter.setToValue(new Date('Jan 1 1850')); + query.addFilter(filter); + + const clone = query.clone(); + + query.setQueryText('different test'); + expect(clone.queryText).toEqual('test'); + + filter.setToValue(new Date('Jan 2 1850')); + expect(query.filters[0].currentData.min).toEqual(new Date('Jan 2 1850')); + expect(clone.filters[0].currentData.min).toEqual(new Date('Jan 1 1850')); + }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index a28077c29..f80fd0931 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -141,7 +141,7 @@ export class QueryModel { this.update.next(); } - /**sortFromParams + /** * reset values to a blank query for the corpus */ reset(): void { @@ -159,12 +159,14 @@ export class QueryModel { */ clone(queryText?: string, addFilter?: SearchFilter) { const newQuery = _.clone(this); // or cloneDeep? - if (queryText !== undefined) { + if (queryText !== undefined) { newQuery.setQueryText(queryText); } if (addFilter) { newQuery.addFilter(addFilter); } + // deep clone filters so they are disconnected from the current query + newQuery.filters = _.cloneDeep(newQuery.filters); return newQuery; } From 7f2d3a001a7d5843ed8e2e8b05cc976eb6aaaa55 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 17:14:47 +0100 Subject: [PATCH 113/262] add tests and docstrings to query model --- frontend/src/app/models/query.spec.ts | 30 ++++++++++++++++++++++----- frontend/src/app/models/query.ts | 6 +++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 1b073644c..54cc09000 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -1,7 +1,7 @@ import { mockField2, mockFieldDate } from '../../mock-data/corpus'; import { Corpus, } from './corpus'; import { QueryModel } from './query'; -import { DateFilter } from './search-filter'; +import { DateFilter, SearchFilter } from './search-filter'; import { convertToParamMap } from '@angular/router'; const corpus: Corpus = { @@ -24,15 +24,39 @@ const corpus: Corpus = { describe('QueryModel', () => { let query: QueryModel; + let filter: SearchFilter; beforeEach(() => { query = new QueryModel(corpus); }); + beforeEach(() => { + filter = new DateFilter(mockFieldDate); + filter.setToValue(new Date('Jan 1 1850')); + + }); + it('should create', () => { expect(query).toBeTruthy(); }); + it('should signal updates', () => { + let updates = 0; + query.update.subscribe(() => updates += 1); + + query.setQueryText('test'); + expect(updates).toBe(1); + + query.addFilter(filter); + expect(updates).toBe(2); + + query.removeFilter(filter); + expect(updates).toBe(3); + + query.reset(); + expect(updates).toBe(4); + }); + it('should convert to an elasticsearch query', () => { expect(query.toEsQuery()).toEqual({ query: { @@ -74,8 +98,6 @@ describe('QueryModel', () => { highlight: null, }); - const filter = new DateFilter(mockFieldDate); - filter.setToValue(new Date('Jan 1 1850')); query.addFilter(filter); expect(query.toRouteParam()).toEqual({ @@ -113,8 +135,6 @@ describe('QueryModel', () => { it('should clone', () => { query.setQueryText('test'); - const filter = new DateFilter(mockFieldDate); - filter.setToValue(new Date('Jan 1 1850')); query.addFilter(filter); const clone = query.clone(); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index f80fd0931..2776ddf9e 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -113,6 +113,7 @@ export class QueryModel { return this.filters.find(filter => filter.corpusField.name === field.name); } + /** remove all filters that apply to a corpus field */ removeFiltersForField(field: CorpusField) { const filterIndex = () => this.filters.findIndex(filter => filter.corpusField.name === field.name); while (filterIndex() !== -1) { @@ -132,6 +133,7 @@ export class QueryModel { this.update.next(); } + /** set the query values from a parameter map */ setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); @@ -157,7 +159,7 @@ export class QueryModel { * make a clone of the current query. * optionally include querytext or a filter for the new query. */ - clone(queryText?: string, addFilter?: SearchFilter) { + clone(queryText: string = undefined, addFilter: SearchFilter = undefined) { const newQuery = _.clone(this); // or cloneDeep? if (queryText !== undefined) { newQuery.setQueryText(queryText); @@ -170,6 +172,7 @@ export class QueryModel { return newQuery; } + /** convert the query to a parameter map */ toRouteParam(): {[param: string]: any} { const queryTextParams = { query: this.queryText || null }; const searchFieldsParams = { fields: this.searchFields?.map(f => f.name).join(',') || null}; @@ -186,6 +189,7 @@ export class QueryModel { }; } + /** convert the query to an elasticsearch query */ toEsQuery(): EsQuery { const searchClause = makeEsSearchClause(this.queryText, this.searchFields); const filters = this.filters.map(filter => filter.toEsFilter()); From 62b96f1389198d3564f8e20dad569740df4aa4e3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 17:21:05 +0100 Subject: [PATCH 114/262] fix deactivation for multiple choice filter --- frontend/src/app/filter/base-filter.component.ts | 2 +- .../src/app/filter/multiple-choice-filter.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 4bd7f274f..8d486abc2 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -43,7 +43,7 @@ export abstract class BaseFilterComponent { beforeEach(() => { fixture = TestBed.createComponent(MultipleChoiceFilterComponent); component = fixture.componentInstance; - component.options = [{value: 'Andy', label: 'Andy', doc_count: 2}, {value: 'Lou', label: 'Lou', doc_count: 3}]; + // component.options = [{value: 'Andy', label: 'Andy', doc_count: 2}, {value: 'Lou', label: 'Lou', doc_count: 3}]; const query = new QueryModel(mockCorpus); component.filter = new PotentialFilter(mockFieldMultipleChoice, query); fixture.detectChanges(); From 341c307370e7e6074e7894bb50eba6ce64389861 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 17:49:00 +0100 Subject: [PATCH 115/262] update search sorting and highlight to query model update --- .../highlight-selector.component.spec.ts | 3 ++ .../search/highlight-selector.component.ts | 9 +++-- .../app/search/search-sorting.component.ts | 35 ++++++++++++------- frontend/src/app/search/search.component.html | 2 +- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/search/highlight-selector.component.spec.ts b/frontend/src/app/search/highlight-selector.component.spec.ts index 15b9bf28b..82fd70d69 100644 --- a/frontend/src/app/search/highlight-selector.component.spec.ts +++ b/frontend/src/app/search/highlight-selector.component.spec.ts @@ -1,5 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { mockCorpus2 } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; +import { QueryModel } from '../models'; import { HighlightSelectorComponent } from './highlight-selector.component'; @@ -14,6 +16,7 @@ describe('HighlightSelectorComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(HighlightSelectorComponent); component = fixture.componentInstance; + component.queryModel = new QueryModel(mockCorpus2); fixture.detectChanges(); }); diff --git a/frontend/src/app/search/highlight-selector.component.ts b/frontend/src/app/search/highlight-selector.component.ts index 064859f0e..260bf62e1 100644 --- a/frontend/src/app/search/highlight-selector.component.ts +++ b/frontend/src/app/search/highlight-selector.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { QueryModel } from '../models'; @@ -9,7 +9,7 @@ const HIGHLIGHT = 200; templateUrl: './highlight-selector.component.html', styleUrls: ['./highlight-selector.component.scss'] }) -export class HighlightSelectorComponent implements OnChanges { +export class HighlightSelectorComponent implements OnChanges, OnDestroy { @Input() queryModel: QueryModel; public highlight: number = HIGHLIGHT; @@ -18,10 +18,15 @@ export class HighlightSelectorComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes.queryModel) { + this.setStateFromQueryModel(); this.queryModel.update.subscribe(this.setStateFromQueryModel.bind(this)); } } + ngOnDestroy(): void { + this.queryModel.setHighlight(undefined); + } + setStateFromQueryModel() { this.highlight = this.queryModel.highlightSize; } diff --git a/frontend/src/app/search/search-sorting.component.ts b/frontend/src/app/search/search-sorting.component.ts index c0ec2a924..af9b8821f 100644 --- a/frontend/src/app/search/search-sorting.component.ts +++ b/frontend/src/app/search/search-sorting.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { CorpusField, QueryModel } from '../models'; const defaultValueType = 'alpha'; @@ -8,23 +8,16 @@ const defaultValueType = 'alpha'; styleUrls: ['./search-sorting.component.scss'], host: { class: 'field has-addons' } }) -export class SearchSortingComponent implements OnChanges { +export class SearchSortingComponent implements OnChanges, OnDestroy { @Input() queryModel: QueryModel; public ascending = true; - public primarySort: CorpusField; public sortField: CorpusField; public valueType: 'alpha' | 'numeric' = defaultValueType; public sortableFields: CorpusField[]; public showFields = false; - private sortData: { - field: CorpusField; - ascending: boolean; - }; - - constructor() {} public get sortType(): SortType { @@ -34,13 +27,27 @@ export class SearchSortingComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes.queryModel) { + this.setSortableFields(); this.queryModel.update.subscribe(this.setStateFromQueryModel.bind(this)); } } - setStateFromQueryModel(queryModel: QueryModel) { - this.sortField = (queryModel.actualSortBy as CorpusField); - this.ascending = queryModel.sortDirection === 'asc'; + ngOnDestroy(): void { + this.queryModel.setSort('default', 'desc'); + } + + setSortableFields() { + this.sortableFields = this.queryModel.corpus.fields.filter(field => field.sortable); + this.setStateFromQueryModel(); + } + + setStateFromQueryModel() { + if (this.queryModel.actualSortBy === 'relevance') { + this.sortField = undefined; + } else { + this.sortField = (this.queryModel.actualSortBy as CorpusField); + } + this.ascending = this.queryModel.sortDirection === 'asc'; } public toggleSortType() { @@ -64,7 +71,9 @@ export class SearchSortingComponent implements OnChanges { } private updateSort() { - this.queryModel.setSort(this.sortField, this.ascending? 'asc': 'desc'); + const sortBy = this.sortField || 'relevance'; + const direction = this.ascending ? 'asc': 'desc'; + this.queryModel.setSort(sortBy, direction); } } diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 30665cb88..512ef2b3f 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -73,7 +73,7 @@ - +
From 62d887e426a85fbfc9b1f9a3fcf45e1bde73e137 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 18:35:37 +0100 Subject: [PATCH 116/262] let barchart react to query model updates --- frontend/src/app/visualization/barchart/barchart.directive.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts index 1d37be71a..b8c680f63 100644 --- a/frontend/src/app/visualization/barchart/barchart.directive.ts +++ b/frontend/src/app/visualization/barchart/barchart.directive.ts @@ -145,6 +145,9 @@ export abstract class BarchartDirective } ngOnChanges(changes: SimpleChanges) { + if (changes.queryModel) { + this.queryModel.update.subscribe(this.refreshChart.bind(this)); + } // new doc counts should be requested if query has changed if (this.changesRequireRefresh(changes)) { this.refreshChart(); From a673d23d6ab3421e7d42db586c8804ba79bdd82f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 18:43:02 +0100 Subject: [PATCH 117/262] use utils functions in more places --- frontend/src/app/history/history.directive.ts | 3 ++- .../src/app/history/search-history/search-history.component.ts | 3 ++- frontend/src/app/search/search.component.ts | 3 ++- frontend/src/app/utils/es-query.ts | 3 ++- frontend/src/app/utils/params.ts | 3 ++- .../word-models/word-similarity/word-similarity.component.ts | 1 - 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/history/history.directive.ts b/frontend/src/app/history/history.directive.ts index a9c5b76d2..deae409f2 100644 --- a/frontend/src/app/history/history.directive.ts +++ b/frontend/src/app/history/history.directive.ts @@ -2,6 +2,7 @@ import { Directive } from '@angular/core'; import { MenuItem } from 'primeng/api'; import { Corpus, Download, QueryDb } from '../models'; import { CorpusService } from '../services'; +import { findByName } from '../utils/utils'; @Directive({ selector: '[iaHistory]' @@ -29,6 +30,6 @@ export class HistoryDirective { corpusTitle(corpusName: string): string { - return this.corpora.find(corpus => corpus.name === corpusName).title || corpusName; + return findByName(this.corpora, corpusName).title || corpusName; } } diff --git a/frontend/src/app/history/search-history/search-history.component.ts b/frontend/src/app/history/search-history/search-history.component.ts index 8d3a4663c..2fa22b480 100644 --- a/frontend/src/app/history/search-history/search-history.component.ts +++ b/frontend/src/app/history/search-history/search-history.component.ts @@ -5,6 +5,7 @@ import { esQueryToQueryModel } from '../../utils/es-query'; import { QueryDb } from '../../models/index'; import { CorpusService, QueryService } from '../../services/index'; import { HistoryDirective } from '../history.directive'; +import { findByName } from '../../utils/utils'; @Component({ selector: 'search-history', @@ -33,7 +34,7 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { } addQueryModel(query?: QueryDb) { - const corpus = this.corpora.find(c => c.name === query.corpus); + const corpus = findByName(this.corpora, query.corpus); query.queryModel = esQueryToQueryModel(query.query_json, corpus); return query; } diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index c42795a9a..fa77e9fc5 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -7,6 +7,7 @@ import { CorpusService, DialogService, } from '../services/index'; import { ParamDirective } from '../param/param-directive'; import { makeContextParams } from '../utils/document-context'; import { AuthService } from '../services/auth.service'; +import { findByName } from '../utils/utils'; @Component({ selector: 'ia-search', @@ -146,7 +147,7 @@ export class SearchComponent extends ParamDirective { const queryModel = new QueryModel(this.corpus); const contextFields = contextSpec.contextFields - .filter(field => ! this.filterFields.find(f => f.name === field.name)); + .filter(field => ! findByName(this.filterFields, field.name)); contextFields.forEach(field => { const filter = field.makeSearchFilter(); diff --git a/frontend/src/app/utils/es-query.ts b/frontend/src/app/utils/es-query.ts index e246fa66a..4b62e8dc9 100644 --- a/frontend/src/app/utils/es-query.ts +++ b/frontend/src/app/utils/es-query.ts @@ -5,6 +5,7 @@ import { BooleanQuery, Corpus, CorpusField, EsFilter, EsSearchClause, MatchAll, QueryModel, SimpleQueryString, SortDirection } from '../models'; import { EsQuery } from '../services'; +import { findByName } from './utils'; import { SearchFilter } from '../models/search-filter'; // conversion from query model -> elasticsearch query language @@ -108,7 +109,7 @@ const filtersFromEsQuery = (query: EsQuery, corpus: Corpus): SearchFilter[] => { const esFilterToSearchFilter = (esFilter: EsFilter, corpus: Corpus): SearchFilter => { const filterType = _.first(_.keys(esFilter)) as 'term'|'terms'|'range'; const fieldName = _.first(_.keys(esFilter[filterType])); - const field = corpus.fields.find(f => f.name === fieldName); + const field = findByName(corpus.fields, fieldName); const filter = field.makeSearchFilter(); filter.data.next(filter.dataFromEsFilter(esFilter as any)); // we know that the esFilter is of the correct type return filter; diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 65f68275b..5b0036bcd 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -1,6 +1,7 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; import { Corpus, CorpusField, QueryModel, SearchFilter, SortBy, SortDirection } from '../models'; +import { findByName } from './utils'; import * as _ from 'lodash'; /** omit keys that mapp to null */ @@ -45,7 +46,7 @@ export const sortSettingsFromParams = (params: ParamMap, corpusFields: CorpusFie if ( sortParam === 'relevance' ) { return [sortParam, sortAscending ? 'asc' : 'desc']; } - sortBy = corpusFields.find(field => field.name === sortParam); + sortBy = findByName(corpusFields, sortParam); } else { sortBy = 'default'; } diff --git a/frontend/src/app/word-models/word-similarity/word-similarity.component.ts b/frontend/src/app/word-models/word-similarity/word-similarity.component.ts index e780d8d8b..b90bcf6f4 100644 --- a/frontend/src/app/word-models/word-similarity/word-similarity.component.ts +++ b/frontend/src/app/word-models/word-similarity/word-similarity.component.ts @@ -1,5 +1,4 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { Chart, ChartData } from 'chart.js'; import * as _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; import { showLoading } from '../../utils/utils'; From f94b6c5a33393e6625014a9a4a11607ee09d003c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 16 Mar 2023 19:01:08 +0100 Subject: [PATCH 118/262] remove multiple choice data from filter manager --- frontend/src/app/filter/filter-manager.component.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index ca46c73c8..5e7d19076 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -34,10 +34,6 @@ export class FilterManagerComponent { public showFilters: boolean; public grayOutFilters: boolean; - public multipleChoiceData: { - [fieldName: string]: any[]; - } = {}; - private _corpus: Corpus; private _queryModel: QueryModel; From 9ba664e003049212a1fa99c84f57b7a055be75e3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 10:38:29 +0100 Subject: [PATCH 119/262] fewer arguments for search functions --- frontend/src/app/search/search-results.component.ts | 7 ++----- frontend/src/app/services/elastic-search.service.ts | 6 ++---- frontend/src/app/services/search.service.ts | 13 ++++--------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/search/search-results.component.ts b/frontend/src/app/search/search-results.component.ts index 01e7fe21b..d890411bf 100644 --- a/frontend/src/app/search/search-results.component.ts +++ b/frontend/src/app/search/search-results.component.ts @@ -95,10 +95,7 @@ export class SearchResultsComponent implements OnChanges { private search() { this.isLoading = true; - this.searchService.search( - this.queryModel, - this.corpus - ).then(results => { + this.searchService.search(this.queryModel).then(results => { this.results = results; this.results.documents.map((d, i) => d.position = i + 1); this.searched(this.queryModel.queryText, this.results.total.value); @@ -119,7 +116,7 @@ export class SearchResultsComponent implements OnChanges { this.isLoading = true; this.fromIndex = searchParameters.from; this.resultsPerPage = searchParameters.size; - this.results = await this.searchService.loadResults(this.corpus, this.queryModel, searchParameters.from, searchParameters.size); + this.results = await this.searchService.loadResults(this.queryModel, searchParameters.from, searchParameters.size); this.results.documents.map( (d, i) => d.position = i + searchParameters.from + 1 ); this.isLoading = false; } diff --git a/frontend/src/app/services/elastic-search.service.ts b/frontend/src/app/services/elastic-search.service.ts index 89f0fdd7a..60a78a691 100644 --- a/frontend/src/app/services/elastic-search.service.ts +++ b/frontend/src/app/services/elastic-search.service.ts @@ -126,14 +126,13 @@ export class ElasticSearchService { public async search( - corpusDefinition: Corpus, queryModel: QueryModel, size?: number, ): Promise { const esQuery = queryModel.toEsQuery(); // Perform the search - const response = await this.execute(corpusDefinition, esQuery, size || this.resultsPerPage); + const response = await this.execute(queryModel.corpus, esQuery, size || this.resultsPerPage); return this.parseResponse(response); } @@ -142,12 +141,11 @@ export class ElasticSearchService { * Load results for requested page */ public async loadResults( - corpusDefinition: Corpus, queryModel: QueryModel, from: number, size: number): Promise { const esQuery = queryModel.toEsQuery(); // Perform the search - const response = await this.execute(corpusDefinition, esQuery, size || this.resultsPerPage, from); + const response = await this.execute(queryModel.corpus, esQuery, size || this.resultsPerPage, from); return this.parseResponse(response); } diff --git a/frontend/src/app/services/search.service.ts b/frontend/src/app/services/search.service.ts index 8c68c2ab8..e5b2f5720 100644 --- a/frontend/src/app/services/search.service.ts +++ b/frontend/src/app/services/search.service.ts @@ -25,31 +25,26 @@ export class SearchService { * Load results for requested page */ public async loadResults( - corpus: Corpus, queryModel: QueryModel, from: number, size: number ): Promise { const results = await this.elasticSearchService.loadResults( - corpus, queryModel, from, size ); - results.fields = corpus.fields.filter((field) => field.resultsOverview); + results.fields = queryModel.corpus.fields.filter((field) => field.resultsOverview); return results; } - public async search( - queryModel: QueryModel, - corpus: Corpus + public async search(queryModel: QueryModel ): Promise { const user = await this.authService.getCurrentUserPromise(); const esQuery = queryModel.toEsQuery(); - const query = new QueryDb(esQuery, corpus.name, user.id); + const query = new QueryDb(esQuery, queryModel.corpus.name, user.id); query.started = new Date(Date.now()); const results = await this.elasticSearchService.search( - corpus, queryModel ); query.total_results = results.total.value; @@ -57,7 +52,7 @@ export class SearchService { this.queryService.save(query); return { - fields: corpus.fields.filter((field) => field.resultsOverview), + fields: queryModel.corpus.fields.filter((field) => field.resultsOverview), total: results.total, documents: results.documents, } as SearchResults; From 85e3ba42627c006d5ea2b031bea01d162a9bb802 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 10:55:27 +0100 Subject: [PATCH 120/262] simplify filter component logic --- .../app/filter/ad-hoc-filter.component.html | 2 +- .../src/app/filter/ad-hoc-filter.component.ts | 12 +---- .../src/app/filter/base-filter.component.ts | 50 ++++++------------- .../app/filter/boolean-filter.component.html | 6 +-- .../app/filter/boolean-filter.component.ts | 13 +---- .../src/app/filter/date-filter.component.html | 2 +- .../src/app/filter/date-filter.component.ts | 17 +------ .../multiple-choice-filter.component.html | 2 +- .../multiple-choice-filter.component.ts | 22 ++++---- .../app/filter/range-filter.component.html | 7 +-- .../src/app/filter/range-filter.component.ts | 4 +- 11 files changed, 46 insertions(+), 91 deletions(-) diff --git a/frontend/src/app/filter/ad-hoc-filter.component.html b/frontend/src/app/filter/ad-hoc-filter.component.html index 9632a8dfd..ecab52e8a 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.html +++ b/frontend/src/app/filter/ad-hoc-filter.component.html @@ -1 +1 @@ -

{{data$ | async}}

+

{{data}}

diff --git a/frontend/src/app/filter/ad-hoc-filter.component.ts b/frontend/src/app/filter/ad-hoc-filter.component.ts index ab260a115..353a29513 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.ts +++ b/frontend/src/app/filter/ad-hoc-filter.component.ts @@ -7,16 +7,8 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './ad-hoc-filter.component.html', styleUrls: ['./ad-hoc-filter.component.scss'] }) -export class AdHocFilterComponent extends BaseFilterComponent { - data: any; +export class AdHocFilterComponent extends BaseFilterComponent { - onFilterSet() {} + onFilterSet(filter: AdHocFilter) {} - getDisplayData(filterData: any) { - return filterData; - } - - getFilterData(data): any { - return data; - } } diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 8d486abc2..01ebd893b 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -1,8 +1,7 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +/* eslint-disable @typescript-eslint/member-ordering */ +import { Component, Input } from '@angular/core'; -import { PotentialFilter, SearchFilter } from '../models/index'; +import { PotentialFilter } from '../models/index'; /** * Filter component receives the corpus fields containing search filters as input @@ -11,52 +10,35 @@ import { PotentialFilter, SearchFilter } from '../models/index'; @Component({ template: '' }) -export abstract class BaseFilterComponent { +export abstract class BaseFilterComponent { + @Input() grayedOut: boolean; + private _filter: PotentialFilter; + constructor() { } + + get data(): FilterData { + return this.filter?.filter.currentData; + } + @Input() get filter() { return this._filter; } set filter(filter: PotentialFilter) { this._filter = filter; - this.data$ = this.filter.filter.data.asObservable().pipe( - map(this.getDisplayData.bind(this)) - ); - this.onFilterSet(filter.filter as SearchFilterClass); + this.onFilterSet(filter.filter); } - @Input() - public grayedOut: boolean; - - /** - * The data of the applied filter transformed to use as input for the value editors. - */ - public data$: Observable; // - - constructor() { } - /** * Trigger a change event. */ - update(data: Data) { - const filterData = this.getFilterData(data); - this.filter.filter.data.next(filterData); - if ((data as any) && (data as any).length === 0) { - this.filter.deactivate(); - } else { - this.filter.activate(); // update called through user input - } + update(data: FilterData) { + this.filter.filter.data.next(data); } /** possible administration when the filter is set, e.g. setting data limits */ - abstract onFilterSet(filter: SearchFilterClass): void; - - /** get the internal data representation from the filter data */ - abstract getDisplayData(filterData): Data; - - /** get the filter data formt from the internal representation */ - abstract getFilterData(data: Data); + abstract onFilterSet(filter: typeof this.filter.filter): void; } diff --git a/frontend/src/app/filter/boolean-filter.component.html b/frontend/src/app/filter/boolean-filter.component.html index a42a821d3..63925cbbc 100644 --- a/frontend/src/app/filter/boolean-filter.component.html +++ b/frontend/src/app/filter/boolean-filter.component.html @@ -1,4 +1,4 @@ -
- - {{data | titlecase }} +
+ + {{data | json | titlecase }}
diff --git a/frontend/src/app/filter/boolean-filter.component.ts b/frontend/src/app/filter/boolean-filter.component.ts index f51c48aeb..7042e1a1f 100644 --- a/frontend/src/app/filter/boolean-filter.component.ts +++ b/frontend/src/app/filter/boolean-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, DoCheck, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { BaseFilterComponent } from './base-filter.component'; import { BooleanFilter } from '../models'; @@ -8,17 +8,8 @@ import { BooleanFilter } from '../models'; templateUrl: './boolean-filter.component.html', styleUrls: ['./boolean-filter.component.scss'] }) -export class BooleanFilterComponent extends BaseFilterComponent { - data: boolean; +export class BooleanFilterComponent extends BaseFilterComponent { onFilterSet(filter: BooleanFilter) {} - getDisplayData(filterData: boolean): boolean { - return filterData; - } - - getFilterData(data: boolean): boolean { - return data; - } - } diff --git a/frontend/src/app/filter/date-filter.component.html b/frontend/src/app/filter/date-filter.component.html index 8104472ff..7652c2338 100644 --- a/frontend/src/app/filter/date-filter.component.html +++ b/frontend/src/app/filter/date-filter.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/src/app/filter/date-filter.component.ts b/frontend/src/app/filter/date-filter.component.ts index bcca134c7..63e5171e3 100644 --- a/frontend/src/app/filter/date-filter.component.ts +++ b/frontend/src/app/filter/date-filter.component.ts @@ -1,6 +1,4 @@ -import { Component, OnInit } from '@angular/core'; - -import * as moment from 'moment'; +import { Component } from '@angular/core'; import { DateFilterData, DateFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @@ -10,7 +8,7 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './date-filter.component.html', styleUrls: ['./date-filter.component.scss'] }) -export class DateFilterComponent extends BaseFilterComponent { +export class DateFilterComponent extends BaseFilterComponent { public minDate: Date; public maxDate: Date; public minYear: number; @@ -22,15 +20,4 @@ export class DateFilterComponent extends BaseFilterComponent +
diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index 4687d43a1..05869850c 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -1,9 +1,9 @@ -import { Component, Input, OnInit, OnChanges } from '@angular/core'; +import { Component } from '@angular/core'; import * as _ from 'lodash'; import { BaseFilterComponent } from './base-filter.component'; -import { SearchFilter, AggregateResult, PotentialFilter, MultipleChoiceFilter, AggregateData, QueryModel, MultipleChoiceFilterOptions } from '../models'; +import { MultipleChoiceFilter, MultipleChoiceFilterOptions } from '../models'; import { SearchService } from '../services'; @Component({ @@ -11,7 +11,7 @@ import { SearchService } from '../services'; templateUrl: './multiple-choice-filter.component.html', styleUrls: ['./multiple-choice-filter.component.scss'] }) -export class MultipleChoiceFilterComponent extends BaseFilterComponent { +export class MultipleChoiceFilterComponent extends BaseFilterComponent { options: { label: string; value: string; doc_count: number }[] = []; constructor(private searchService: SearchService) { @@ -20,14 +20,8 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent this.deactivateWhenEmpty()); - getFilterData(data: string[]): string[] { - return data; } private async getOptions(): Promise { @@ -44,4 +38,12 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent - {{data[0]}} - {{data[1]}} - +
+ {{data.min}} - {{data.max}} +
diff --git a/frontend/src/app/filter/range-filter.component.ts b/frontend/src/app/filter/range-filter.component.ts index eecba78c0..162c2ab7f 100644 --- a/frontend/src/app/filter/range-filter.component.ts +++ b/frontend/src/app/filter/range-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { RangeFilterData, RangeFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @@ -8,7 +8,7 @@ import { BaseFilterComponent } from './base-filter.component'; templateUrl: './range-filter.component.html', styleUrls: ['./range-filter.component.scss'] }) -export class RangeFilterComponent extends BaseFilterComponent<[number, number], RangeFilter> { +export class RangeFilterComponent extends BaseFilterComponent { min: number; max: number; From b72f4fcb66be8db1ed8c1ab9b15116b7b1a9c96b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 11:02:26 +0100 Subject: [PATCH 121/262] remove search-filter-old.ts --- frontend/src/app/models/filter-management.ts | 2 +- frontend/src/app/models/search-filter-old.ts | 98 ------------------- .../src/app/models/search-filter-options.ts | 2 + 3 files changed, 3 insertions(+), 99 deletions(-) delete mode 100644 frontend/src/app/models/search-filter-old.ts diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index e4563d316..9ff61e31b 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -1,7 +1,7 @@ import { CorpusField } from './corpus'; import { QueryModel } from './query'; import { SearchFilter } from './search-filter'; -import { SearchFilterType } from './search-filter-old'; +import { SearchFilterType } from './search-filter-options'; export class PotentialFilter { filter: SearchFilter; diff --git a/frontend/src/app/models/search-filter-old.ts b/frontend/src/app/models/search-filter-old.ts deleted file mode 100644 index cd3b9606f..000000000 --- a/frontend/src/app/models/search-filter-old.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { CorpusField } from './corpus'; - -export interface SearchFilter { - fieldName: string; - description: string; - useAsFilter: boolean; - reset?: boolean; - grayedOut?: boolean; - adHoc?: boolean; - defaultData?: T; - currentData: T; -}; - -export type SearchFilterData = BooleanFilterData | MultipleChoiceFilterData | RangeFilterData | DateFilterData; - -export interface BooleanFilterData { - filterType: 'BooleanFilter'; - checked: boolean; -} -export interface MultipleChoiceFilterData { - filterType: 'MultipleChoiceFilter'; - optionCount?: number; - selected: string[]; -} -export interface RangeFilterData { - filterType: 'RangeFilter'; - min: number; - max: number; -} -export interface DateFilterData { - filterType: 'DateFilter'; - /** minimum of date range, format: yyyy-MM-dd */ - min: string; - /** maximum of date range, format: yyyy-MM-dd */ - max: string; -} - -export type SearchFilterType = SearchFilterData['filterType']; - -export function searchFilterDataFromSettings(filterType: SearchFilterType|undefined, value: string[], field: CorpusField): SearchFilterData { - switch (filterType) { - case 'BooleanFilter': - return { filterType, checked: value[0] === 'true' }; - case 'MultipleChoiceFilter': - return { filterType, selected: value }; - case 'RangeFilter': { - const [min, max] = parseMinMax(value); - return { filterType, min: parseFloat(min), max: parseFloat(max) }; - } - case 'DateFilter': { - const [min, max] = parseMinMax(value); - return { filterType, min, max }; - } - case undefined: { - return searchFilterDataFromField(field, value); - } - } -}; - -export const searchFilterDataFromField = (field: CorpusField, value: string[]): SearchFilterData => { - switch (field.mappingType) { - case 'boolean': - return { filterType: 'BooleanFilter', checked: value[0] === 'true' }; - case 'date': { - const [min, max] = parseMinMax(value); - return { filterType: 'DateFilter', min, max }; - } - case 'integer': { - const [min, max] = parseMinMax(value); - return { filterType: 'RangeFilter', min: parseFloat(min), max: parseFloat(max) }; - } - case 'keyword': { - return { filterType: 'MultipleChoiceFilter', selected: value.map(encodeURIComponent) }; - } - } -}; - -export const parseMinMax = (value: string[]): [string, string] => { - const term = value[0]; - if (term.split(':').length === 2) { - return term.split(':') as [string, string]; - } else if (value.length === 1) { - return [term, term]; - } else { - return [value[0], value[1]]; - } -}; - -export function contextFilterFromField(field: CorpusField, value?: string): SearchFilter { - const currentValue = value ? searchFilterDataFromField(field, [value]) : undefined; - return { - fieldName: field.name, - description: `Search only within this ${field.displayName}`, - useAsFilter: true, - adHoc: true, - currentData: currentValue - }; -} diff --git a/frontend/src/app/models/search-filter-options.ts b/frontend/src/app/models/search-filter-options.ts index 7a874546e..5cc4a4343 100644 --- a/frontend/src/app/models/search-filter-options.ts +++ b/frontend/src/app/models/search-filter-options.ts @@ -1,5 +1,7 @@ // Types for serialised filter options for a corpus by the API +export type SearchFilterType = 'DateFilter' | 'MultipleChoiceFilter' | 'RangeFilter' | 'BooleanFilter'; + export interface HasDescription { description: string; } From cbcfd4295f6fffb04299bf7dc7555ef50dadbb22 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 11:03:55 +0100 Subject: [PATCH 122/262] clean up sort types --- frontend/src/app/models/index.ts | 2 +- frontend/src/app/models/{sort-event.ts => sort.ts} | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) rename frontend/src/app/models/{sort-event.ts => sort.ts} (60%) diff --git a/frontend/src/app/models/index.ts b/frontend/src/app/models/index.ts index 53550146c..06b8a1a66 100644 --- a/frontend/src/app/models/index.ts +++ b/frontend/src/app/models/index.ts @@ -5,7 +5,7 @@ export * from './search-filter'; export * from './search-filter-options'; export * from './filter-management'; export * from './search-results'; -export * from './sort-event'; +export * from './sort'; export * from './user'; export * from './user-role'; export * from './visualization'; diff --git a/frontend/src/app/models/sort-event.ts b/frontend/src/app/models/sort.ts similarity index 60% rename from frontend/src/app/models/sort-event.ts rename to frontend/src/app/models/sort.ts index fd1daba5b..cfbcd8dac 100644 --- a/frontend/src/app/models/sort-event.ts +++ b/frontend/src/app/models/sort.ts @@ -1,9 +1,4 @@ import { CorpusField } from './corpus'; -export interface SortEvent { - ascending: boolean; - field: CorpusField | undefined; -} - export type SortBy = CorpusField | 'relevance' | 'default'; export type SortDirection = 'asc'|'desc'; From a8c3d61811e42e5ab58e6fe9409ee85788042771 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 11:05:53 +0100 Subject: [PATCH 123/262] remove queryModel.reset --- frontend/src/app/models/query.spec.ts | 2 -- frontend/src/app/models/query.ts | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 54cc09000..9e97b18bb 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -53,8 +53,6 @@ describe('QueryModel', () => { query.removeFilter(filter); expect(updates).toBe(3); - query.reset(); - expect(updates).toBe(4); }); it('should convert to an elasticsearch query', () => { diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 2776ddf9e..a731917e7 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -143,18 +143,6 @@ export class QueryModel { this.update.next(); } - /** - * reset values to a blank query for the corpus - */ - reset(): void { - this.queryText = undefined; - this.searchFields = undefined; - this.filters = []; - this.sortBy = 'default'; - this.sortDirection = undefined; - this.update.next(); - } - /** * make a clone of the current query. * optionally include querytext or a filter for the new query. From 04be235f959f5e16f28228aadce1873ff2c6ec0e Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 11:16:32 +0100 Subject: [PATCH 124/262] adjust es query to match backend expectations --- frontend/src/app/models/query.spec.ts | 13 +++++++++---- frontend/src/app/models/query.ts | 3 +-- frontend/src/app/utils/es-query.ts | 10 ++++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 9e97b18bb..f9afa39ab 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -66,10 +66,15 @@ describe('QueryModel', () => { expect(query.toEsQuery()).toEqual({ query: { - simple_query_string: { - query: 'test', - lenient: true, - default_operator: 'or', + bool: { + must: { + simple_query_string: { + query: 'test', + lenient: true, + default_operator: 'or', + } + }, + filter: [] } } }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index a731917e7..01384c8c2 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -179,9 +179,8 @@ export class QueryModel { /** convert the query to an elasticsearch query */ toEsQuery(): EsQuery { - const searchClause = makeEsSearchClause(this.queryText, this.searchFields); const filters = this.filters.map(filter => filter.toEsFilter()); - const query = combineSearchClauseAndFilters(searchClause, filters); + const query = combineSearchClauseAndFilters(this.queryText, filters, this.searchFields); const sort = makeSortSpecification(this.actualSortBy, this.sortDirection); const highlight = makeHighlightSpecification(this.corpus, this.queryText, this.highlightSize); diff --git a/frontend/src/app/utils/es-query.ts b/frontend/src/app/utils/es-query.ts index 4b62e8dc9..1143bbee1 100644 --- a/frontend/src/app/utils/es-query.ts +++ b/frontend/src/app/utils/es-query.ts @@ -44,8 +44,14 @@ export const makeBooleanQuery = (query: EsSearchClause, filters: EsFilter[]): Bo } }); -export const combineSearchClauseAndFilters = (searchClause: EsSearchClause, filters?: EsFilter[]): EsQuery => { - const query = (filters && filters.length) ? makeBooleanQuery(searchClause, filters) : searchClause; +export const combineSearchClauseAndFilters = (queryText: string, filters: EsFilter[], searchFields?: CorpusField[]): EsQuery => { + let query: MatchAll | BooleanQuery; + if (queryText || filters.length) { + const searchClause = makeEsSearchClause(queryText, searchFields); + query = makeBooleanQuery(searchClause, filters); + } else { + query = matchAll; + } return { query }; }; From d79c610602b8ce50bdf132eb47b65a68cd4ac414 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 11:46:44 +0100 Subject: [PATCH 125/262] improve filter activation logic --- .../src/app/filter/base-filter.component.ts | 5 +-- .../src/app/filter/date-filter.component.html | 4 +-- .../src/app/filter/date-filter.component.ts | 7 ++++ .../app/filter/filter-manager.component.html | 18 +++++------ .../app/filter/filter-manager.component.ts | 27 +++++++++++++--- .../multiple-choice-filter.component.ts | 11 ------- .../src/app/models/filter-management.spec.ts | 29 ++++++++++++++++- frontend/src/app/models/filter-management.ts | 32 +++++++++++++++---- frontend/src/app/models/search-filter.ts | 11 +++++-- frontend/src/app/search/search.component.ts | 5 +-- 10 files changed, 107 insertions(+), 42 deletions(-) diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 01ebd893b..3f5728753 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/member-ordering */ import { Component, Input } from '@angular/core'; +import * as _ from 'lodash'; import { PotentialFilter } from '../models/index'; @@ -35,10 +36,10 @@ export abstract class BaseFilterComponent { * Trigger a change event. */ update(data: FilterData) { - this.filter.filter.data.next(data); + this.filter.set(data); } /** possible administration when the filter is set, e.g. setting data limits */ - abstract onFilterSet(filter: typeof this.filter.filter): void; + abstract onFilterSet(filter): void; } diff --git a/frontend/src/app/filter/date-filter.component.html b/frontend/src/app/filter/date-filter.component.html index 7652c2338..d656d84ab 100644 --- a/frontend/src/app/filter/date-filter.component.html +++ b/frontend/src/app/filter/date-filter.component.html @@ -1,8 +1,8 @@
+ (onSelect)="updateProperty('min', $event)" dateFormat="dd-mm-yy"> + (onSelect)="updateProperty('max', $event)" dateFormat="dd-mm-yy">
diff --git a/frontend/src/app/filter/date-filter.component.ts b/frontend/src/app/filter/date-filter.component.ts index 63e5171e3..9c88ee5f1 100644 --- a/frontend/src/app/filter/date-filter.component.ts +++ b/frontend/src/app/filter/date-filter.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import * as _ from 'lodash'; import { DateFilterData, DateFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; @@ -20,4 +21,10 @@ export class DateFilterComponent extends BaseFilterComponent { this.minYear = this.minDate.getFullYear(); this.maxYear = this.maxDate.getFullYear(); } + + updateProperty(property: 'min'|'max', date: Date) { + const value = _.merge(_.clone(this.data), {[property]: date}); + this.update(value); + } + } diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index efbef00bd..6a66c39ac 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -4,21 +4,21 @@

Filters

- -

-
+
@@ -26,15 +26,15 @@

Filters

}}

- - diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 5e7d19076..40ea343aa 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -2,8 +2,10 @@ import { Component, Input } from '@angular/core'; import * as _ from 'lodash'; +import { combineLatest, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; -import { PotentialFilter, Corpus, SearchFilter, QueryModel} from '../models/index'; +import { PotentialFilter, Corpus, SearchFilter, QueryModel } from '../models/index'; @Component({ selector: 'ia-filter-manager', @@ -31,7 +33,6 @@ export class FilterManagerComponent { public potentialFilters: PotentialFilter[] = []; - public showFilters: boolean; public grayOutFilters: boolean; private _corpus: Corpus; @@ -44,6 +45,24 @@ export class FilterManagerComponent { return this.queryModel.filters; } + get anyActiveFilters$(): Observable { + if (this.potentialFilters) { + const statuses = this.potentialFilters.map(filter => filter.useAsFilter); + return combineLatest(statuses).pipe( + map(values => _.some(values)), + ); + } + } + + get anyNonDefaultFilters$(): Observable { + if (this.potentialFilters) { + const statuses = this.potentialFilters.map(filter => filter.filter.isDefault$); + return combineLatest(statuses).pipe( + map(values => !_.every(values)), + ); + } + } + setPotentialFilters() { if (this.corpus && this.queryModel) { this.potentialFilters = this.corpus.fields.map(field => new PotentialFilter(field, this.queryModel)); @@ -56,13 +75,13 @@ export class FilterManagerComponent { } else { // if we don't have active filters, set all filters to active which don't use default data const filtersWithSettings = this.potentialFilters.filter(pFilter => - pFilter.filter.currentData === pFilter.filter.defaultData); + !_.isEqual(pFilter.filter.currentData, pFilter.filter.defaultData)); filtersWithSettings.forEach(filter => filter.toggle()); } } public resetAllFilters() { - this.activeFilters.forEach(filter => { + this.potentialFilters.forEach(filter => { filter.reset(); }); } diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index 05869850c..e671c59d5 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -20,8 +20,6 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent onFilterSet(filter: MultipleChoiceFilter): void { this.getOptions(); - this.filter.filter.data.subscribe(data => this.deactivateWhenEmpty()); - } private async getOptions(): Promise { @@ -37,13 +35,4 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent ) ); } - - private deactivateWhenEmpty() { - if (this.filter.filter.data.value.length === 0) { - this.filter.deactivate(); - } else { - this.filter.activate(); // update called through user input - } - } - } diff --git a/frontend/src/app/models/filter-management.spec.ts b/frontend/src/app/models/filter-management.spec.ts index 38064b6a5..9189dca22 100644 --- a/frontend/src/app/models/filter-management.spec.ts +++ b/frontend/src/app/models/filter-management.spec.ts @@ -1,4 +1,4 @@ -import { mockCorpus, mockField } from '../../mock-data/corpus'; +import { mockCorpus, mockCorpus3, mockField, mockFieldDate, mockFieldMultipleChoice } from '../../mock-data/corpus'; import { PotentialFilter } from './filter-management'; import { QueryModel } from './query'; @@ -14,6 +14,7 @@ describe('PotentialFilter', () => { const field = mockField; const query = new QueryModel(mockCorpus); const potentialFilter = new PotentialFilter(field, query); + potentialFilter.filter.data.next(true); expect(query.filters.length).toBe(0); potentialFilter.toggle(); @@ -21,4 +22,30 @@ describe('PotentialFilter', () => { potentialFilter.toggle(); expect(query.filters.length).toBe(0); }); + + it('should deactivate when a date filter resets', () => { + const field = mockFieldDate; + const query = new QueryModel(mockCorpus3); + const potentialFilter = new PotentialFilter(field, query); + + potentialFilter.filter.setToValue('Jan 1 1850'); + potentialFilter.activate(); + expect(query.filters.length).toBe(1); + + potentialFilter.filter.reset(); + expect(query.filters.length).toBe(0); + }); + + it('should deactivate when a multiple choice filter resets', () => { + const field = mockFieldMultipleChoice; + const query = new QueryModel(mockCorpus3); + const potentialFilter = new PotentialFilter(field, query); + + potentialFilter.filter.setToValue('test'); + potentialFilter.activate(); + expect(query.filters.length).toBe(1); + + potentialFilter.filter.data.next([]); + expect(query.filters.length).toBe(0); + }); }); diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index 9ff61e31b..22fe57f61 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -1,3 +1,5 @@ +import * as _ from 'lodash'; +import { BehaviorSubject } from 'rxjs'; import { CorpusField } from './corpus'; import { QueryModel } from './query'; import { SearchFilter } from './search-filter'; @@ -5,19 +7,19 @@ import { SearchFilterType } from './search-filter-options'; export class PotentialFilter { filter: SearchFilter; - useAsFilter: boolean; + useAsFilter = new BehaviorSubject(false); showReset?: boolean; grayedOut?: boolean; adHoc?: boolean; constructor(public corpusField: CorpusField, public queryModel: QueryModel) { this.filter = corpusField.makeSearchFilter(); - this.useAsFilter = false; if (!corpusField.filterOptions) { this.adHoc = true; } else { this.adHoc = false; } + this.filter.isDefault$.subscribe(this.deactivateWhenDefault.bind(this)); } get filterType(): SearchFilterType { @@ -25,8 +27,8 @@ export class PotentialFilter { } toggle() { - this.useAsFilter = !this.useAsFilter; - if (this.useAsFilter) { + this.useAsFilter.next(!this.useAsFilter.value); + if (this.useAsFilter.value) { this.queryModel.addFilter(this.filter); } else { this.queryModel.removeFilter(this.filter); @@ -34,19 +36,35 @@ export class PotentialFilter { } deactivate() { - if (this.useAsFilter) { + if (this.useAsFilter.value) { this.toggle(); } } activate() { - if (!this.useAsFilter) { + if (!this.useAsFilter.value) { this.toggle(); } } + set(data: any) { + if (!_.isEqual(data, this.filter.currentData)) { + this.filter.data.next(data); + + if (!_.isEqual(data, this.filter.defaultData)) { + this.activate(); + } + } + } + reset() { - this.deactivate(); this.filter.reset(); } + + /** called after filter updates: deactivate the filter if the filter uses default data */ + private deactivateWhenDefault(isDefault: boolean) { + if (isDefault) { + this.deactivate(); + } + } } diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 003a83545..eb1a2e7e4 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import * as moment from 'moment'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { CorpusField } from './corpus'; import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, EsTermFilter } from './elasticsearch'; import { BooleanFilterOptions, DateFilterOptions, FilterOptions, MultipleChoiceFilterOptions, @@ -21,6 +22,12 @@ abstract class AbstractSearchFilter { return this.data?.value; } + get isDefault$(): Observable { + return this.data.asObservable().pipe( + map(data => _.isEqual(data, this.defaultData)) + ); + } + reset() { this.data.next(this.defaultData); } @@ -42,7 +49,7 @@ abstract class AbstractSearchFilter { toRouteParam(): {[param: string]: any} { return { - [this.corpusField.name]: this.dataToString(this.currentData) + [this.corpusField.name]: this.dataToString(this.currentData) || null }; } diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index fa77e9fc5..906d5ee3f 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -31,10 +31,7 @@ export class SearchComponent extends ParamDirective { * Whether the total number of hits exceeds the download limit. */ public hasLimitedResults = false; - /** - * Hide the filters by default, unless an existing search is opened containing filters. - */ - public showFilters = true; + public user: User; protected corpusSubscription: Subscription; From 002576403dc0a44d704d9c31b897b545f46056a6 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 17 Mar 2023 13:09:29 +0100 Subject: [PATCH 126/262] remove unused mock data --- frontend/src/mock-data/query-model.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 frontend/src/mock-data/query-model.ts diff --git a/frontend/src/mock-data/query-model.ts b/frontend/src/mock-data/query-model.ts deleted file mode 100644 index 67fead3ab..000000000 --- a/frontend/src/mock-data/query-model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { QueryModel } from '../app/models'; - -export const mockQueryModel: QueryModel = { - queryText: 'Gouda', - filters: [] -}; From e22ee5f04bc1fd07e1022ab54ae56728077f7962 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 11:36:28 +0100 Subject: [PATCH 127/262] fix imports after upstream merge --- frontend/src/app/services/elastic-search.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/services/elastic-search.service.ts b/frontend/src/app/services/elastic-search.service.ts index 60a78a691..7f5f2b58b 100644 --- a/frontend/src/app/services/elastic-search.service.ts +++ b/frontend/src/app/services/elastic-search.service.ts @@ -5,7 +5,8 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { FoundDocument, Corpus, QueryModel, SearchResults, - AggregateQueryFeedback, EsSearchClause, BooleanQuery, EsFilter + AggregateQueryFeedback, EsSearchClause, BooleanQuery, + EsFilter } from '../models/index'; import * as _ from 'lodash'; From c730a9efd4079e6854d6dc28acdf9486c93c8544 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 11:43:49 +0100 Subject: [PATCH 128/262] add omitnullparameters function --- frontend/src/app/utils/params.spec.ts | 11 +++++++++++ frontend/src/app/utils/params.ts | 1 - 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/utils/params.spec.ts b/frontend/src/app/utils/params.spec.ts index 2d1e0ece9..2e74b2291 100644 --- a/frontend/src/app/utils/params.spec.ts +++ b/frontend/src/app/utils/params.spec.ts @@ -1,5 +1,6 @@ import { convertToParamMap } from '@angular/router'; import { highlightFromParams, omitNullParameters, searchFieldsFromParams } from './params'; +import { omitNullParameters } from './params'; describe('searchFieldsFromParams', () => { it('should parse field parameters', () => { @@ -27,3 +28,13 @@ describe('omitNullParameters', () => { ); }); }); + +describe('omitNullParameters', () => { + it('should omit null parameters', () => { + const p = { a: null, b: 1, c: 'test' }; + + expect(omitNullParameters(p)).toEqual( + { b: 1, c: 'test' } + ); + }); +}); diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 5b0036bcd..5a598ea11 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -2,7 +2,6 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; import { Corpus, CorpusField, QueryModel, SearchFilter, SortBy, SortDirection } from '../models'; import { findByName } from './utils'; -import * as _ from 'lodash'; /** omit keys that mapp to null */ export const omitNullParameters = (params: {[key: string]: any}): {[key: string]: any} => { From 653ff2f519c36662937eeb23a5bde56e9c195819 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 11:50:50 +0100 Subject: [PATCH 129/262] add toRoute to query model --- frontend/src/app/models/query.spec.ts | 12 ++++++++++++ frontend/src/app/models/query.ts | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index f9afa39ab..0d6339ef6 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -136,6 +136,18 @@ describe('QueryModel', () => { expect(query.filters.length).toBe(1); }); + it('should formulate a link', () => { + query.setQueryText('test'); + query.addFilter(filter); + + const link = query.toRoute(); + expect(link).toEqual([ + '/search', + 'mock-corpus', + { query: 'test', date: '1850-01-01:1850-01-01' } + ]); + }); + it('should clone', () => { query.setQueryText('test'); query.addFilter(filter); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 01384c8c2..40e74ae31 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -3,9 +3,9 @@ import * as _ from 'lodash'; import { Subject } from 'rxjs'; import { Corpus, CorpusField, SortBy, SortDirection } from '../models/index'; import { EsQuery } from '../services'; -import { combineSearchClauseAndFilters, makeEsSearchClause, makeHighlightSpecification, makeSortSpecification } from '../utils/es-query'; -import { filtersFromParams, highlightFromParams, queryFiltersToParams, queryFromParams, searchFieldsFromParams, sortSettingsFromParams, - sortSettingsToParams } from '../utils/params'; +import { combineSearchClauseAndFilters, makeHighlightSpecification, makeSortSpecification } from '../utils/es-query'; +import { filtersFromParams, highlightFromParams, omitNullParameters, queryFiltersToParams, + queryFromParams, searchFieldsFromParams, sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; import { sortByDefault } from '../utils/sort'; import { SearchFilter } from './search-filter'; @@ -177,6 +177,12 @@ export class QueryModel { }; } + /** convert this link to an array describing the route, which can be used to create a routerlink */ + toRoute(): any[] { + const params = omitNullParameters(this.toRouteParam()); + return ['/search', this.corpus.name, params]; + } + /** convert the query to an elasticsearch query */ toEsQuery(): EsQuery { const filters = this.filters.map(filter => filter.toEsFilter()); From 84ce03e2ee4d1ba3fa2bafdcb8ca14c1f058b07d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 12:04:27 +0100 Subject: [PATCH 130/262] add function for document context link --- .../app/search/search-results.component.html | 2 +- .../app/search/search-results.component.ts | 13 ++-- frontend/src/app/search/search.component.html | 2 +- frontend/src/app/search/search.component.ts | 23 -------- .../src/app/utils/document-context.spec.ts | 59 ++----------------- frontend/src/app/utils/document-context.ts | 32 +++++----- frontend/src/mock-data/corpus.ts | 8 ++- 7 files changed, 36 insertions(+), 103 deletions(-) diff --git a/frontend/src/app/search/search-results.component.html b/frontend/src/app/search/search-results.component.html index 3db19c305..c61514805 100644 --- a/frontend/src/app/search/search-results.component.html +++ b/frontend/src/app/search/search-results.component.html @@ -99,7 +99,7 @@

Link   - diff --git a/frontend/src/app/search/search-results.component.ts b/frontend/src/app/search/search-results.component.ts index d890411bf..65cf78dfd 100644 --- a/frontend/src/app/search/search-results.component.ts +++ b/frontend/src/app/search/search-results.component.ts @@ -6,6 +6,7 @@ import { SearchService } from '../services'; import { ShowError } from '../error/error.component'; import * as _ from 'lodash'; import { faBookOpen, faArrowLeft, faArrowRight, faLink } from '@fortawesome/free-solid-svg-icons'; +import { makeContextParams } from '../utils/document-context'; @Component({ selector: 'ia-search-results', @@ -37,9 +38,6 @@ export class SearchResultsComponent implements OnChanges { @Output('searched') public searchedEvent = new EventEmitter(); - @Output('viewContext') - public contextEvent = new EventEmitter(); - public isLoading = false; public isScrolledDown: boolean; @@ -139,11 +137,6 @@ export class SearchResultsComponent implements OnChanges { this.documentTabIndex = 0; } - public goToContext(document: FoundDocument) { - this.showDocument = false; - this.contextEvent.emit(document); - } - get contextDisplayName(): string { if (this.corpus && this.corpus.documentContext) { return this.corpus.documentContext.displayName; @@ -197,4 +190,8 @@ export class SearchResultsComponent implements OnChanges { } return false; } + + contextParams(document: FoundDocument) { + return makeContextParams(document, this.corpus); + } } diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 512ef2b3f..63b19a840 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -71,7 +71,7 @@

+ (searched)="onSearched($event)"> diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index 906d5ee3f..c0812e0fc 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -5,9 +5,7 @@ import { ActivatedRoute, Router, ParamMap } from '@angular/router'; import { Corpus, CorpusField, ResultOverview, QueryModel, User } from '../models/index'; import { CorpusService, DialogService, } from '../services/index'; import { ParamDirective } from '../param/param-directive'; -import { makeContextParams } from '../utils/document-context'; import { AuthService } from '../services/auth.service'; -import { findByName } from '../utils/utils'; @Component({ selector: 'ia-search', @@ -136,25 +134,4 @@ export class SearchComponent extends ParamDirective { this.setParams(this.queryModel.toRouteParam()); }); } - - // eslint-disable-next-line @typescript-eslint/member-ordering - public goToContext(contextValues: any) { - const contextSpec = this.corpus.documentContext; - - const queryModel = new QueryModel(this.corpus); - - const contextFields = contextSpec.contextFields - .filter(field => ! findByName(this.filterFields, field.name)); - - contextFields.forEach(field => { - const filter = field.makeSearchFilter(); - filter.setToValue(contextValues[field.name]); - queryModel.addFilter(filter); - }); - - queryModel.sortBy = contextSpec.sortField; - queryModel.sortDirection = contextSpec.sortDirection; - - this.setParams(queryModel.toRouteParam()); - } } diff --git a/frontend/src/app/utils/document-context.spec.ts b/frontend/src/app/utils/document-context.spec.ts index 39464b447..3a69daa85 100644 --- a/frontend/src/app/utils/document-context.spec.ts +++ b/frontend/src/app/utils/document-context.spec.ts @@ -1,56 +1,9 @@ -import { mockField, mockField2, mockField3 } from '../../mock-data/corpus'; -import { Corpus, CorpusField, FoundDocument } from '../models'; +import { mockCorpus3 } from '../../mock-data/corpus'; +import { FoundDocument } from '../models'; import { makeContextParams } from './document-context'; describe('document context utils', () => { - const dateField: CorpusField = { - name: 'date', - displayName: 'Date', - displayType: 'date', - mappingType: 'date', - description: '', - searchable: false, - sortable: true, - hidden: false, - downloadable: true, - primarySort: false, - searchFilter: { - fieldName: 'date', - description: '', - useAsFilter: false, - currentData: { - filterType: 'DateFilter', - min: '1800-01-01', - max: '1900-01-01' - } - }, - }; - - const corpus: Corpus = { - name: 'mock-corpus', - title: 'Mock corpus', - serverName: 'default', - description: '', - index: 'mock-corpus', - minDate: new Date('1800-01-01'), - maxDate: new Date('1900-01-01'), - image: '', - scan_image_type: '', - allow_image_download: true, - word_models_present: false, - documentContext: { - contextFields: [dateField], - displayName: 'edition', - sortField: mockField3, - sortDirection: 'asc', - }, - fields: [ - mockField, - mockField2, - mockField3, - dateField, - ] - }; + const corpus = mockCorpus3; const document: FoundDocument = { id: '1', @@ -64,11 +17,11 @@ describe('document context utils', () => { }; it('should create a document context link', () => { - const params = makeContextParams(document, corpus); - - expect(params).toEqual({ + const link = makeContextParams(document, corpus); + expect(link).toEqual({ date: '1900-01-01:1900-01-01', sort: 'ordering,asc' }); + }); }); diff --git a/frontend/src/app/utils/document-context.ts b/frontend/src/app/utils/document-context.ts index e843c3e48..42b1325fe 100644 --- a/frontend/src/app/utils/document-context.ts +++ b/frontend/src/app/utils/document-context.ts @@ -1,23 +1,23 @@ -import { contextFilterFromField, Corpus, FoundDocument } from '../models'; -import { omitNullParameters, searchFiltersToParams, sortSettingsToParams } from './params'; +import { Corpus, FoundDocument, QueryModel } from '../models'; -export const makeContextParams = (document: FoundDocument, corpus: Corpus): any => { - const contextSpec = corpus.documentContext; - - const queryText = null; +const documentContextQuery = (corpus: Corpus, document: FoundDocument): QueryModel => { + const queryModel = new QueryModel(corpus); - const contextFields = contextSpec.contextFields; + const spec = corpus.documentContext; - contextFields.forEach(field => { - field.searchFilter = contextFilterFromField(field, document.fieldValues[field.name]); + spec.contextFields.forEach(field => { + const filter = field.makeSearchFilter(); + filter.setToValue(document.fieldValues[field.name]); + queryModel.addFilter(filter); }); - const filterParams = searchFiltersToParams(contextFields); - const sortParams = sortSettingsToParams( - contextSpec.sortField, - contextSpec.sortDirection - ); + queryModel.sortBy = spec.sortField || 'default'; + queryModel.sortDirection = spec.sortDirection; - const params = { query: queryText, ...filterParams, ...sortParams }; - return omitNullParameters(params); + return queryModel; +}; + +export const makeContextParams = (document: FoundDocument, corpus: Corpus): any => { + const queryModel = documentContextQuery(corpus, document); + return queryModel.toRouteParam(); }; diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index 37c4417c6..5150c1825 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -177,7 +177,13 @@ export const mockCorpus3: Corpus = { scan_image_type: 'pdf', allow_image_download: false, word_models_present: false, - fields: [mockField, mockField2, mockField3, mockFieldDate] + fields: [mockField, mockField2, mockField3, mockFieldDate], + documentContext: { + contextFields: [mockFieldDate], + displayName: 'day', + sortField: mockField3, + sortDirection: 'asc' + } }; export class CorpusServiceMock { From 3fcf0553d1d765a685eb810c7c601199f75bea5c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 12:12:34 +0100 Subject: [PATCH 131/262] cleaner code for default sort --- frontend/src/app/utils/params.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 5a598ea11..74df1cf5d 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -46,11 +46,9 @@ export const sortSettingsFromParams = (params: ParamMap, corpusFields: CorpusFie return [sortParam, sortAscending ? 'asc' : 'desc']; } sortBy = findByName(corpusFields, sortParam); - } else { - sortBy = 'default'; } return [ - sortBy, + sortBy || 'default', sortAscending ? 'asc' : 'desc' ]; }; From e86f9a2b3648323826dd8cae0dd996cdf5413be8 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 14:34:28 +0100 Subject: [PATCH 132/262] set initial filter from query model if possible --- frontend/src/app/models/filter-management.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index 22fe57f61..8e19f7cc2 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -13,7 +13,13 @@ export class PotentialFilter { adHoc?: boolean; constructor(public corpusField: CorpusField, public queryModel: QueryModel) { - this.filter = corpusField.makeSearchFilter(); + if (queryModel.filterForField(corpusField)) { + this.filter = queryModel.filterForField(corpusField); + this.useAsFilter.next(true); + } else { + this.filter = corpusField.makeSearchFilter(); + } + if (!corpusField.filterOptions) { this.adHoc = true; } else { From 37088b97ae646fdeb1c986995fc21d796ff2f2cd Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 12:12:45 +0100 Subject: [PATCH 133/262] use document context link --- frontend/src/app/models/query.spec.ts | 7 +------ frontend/src/app/models/query.ts | 8 +++----- frontend/src/app/search/search.component.ts | 10 +++++----- frontend/src/app/utils/document-context.spec.ts | 5 ++--- frontend/src/app/utils/document-context.ts | 3 ++- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 0d6339ef6..a18165eae 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -140,12 +140,7 @@ describe('QueryModel', () => { query.setQueryText('test'); query.addFilter(filter); - const link = query.toRoute(); - expect(link).toEqual([ - '/search', - 'mock-corpus', - { query: 'test', date: '1850-01-01:1850-01-01' } - ]); + expect(query.toQueryParams()).toEqual({ query: 'test', date: '1850-01-01:1850-01-01' }); }); it('should clone', () => { diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 40e74ae31..451efd843 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -162,7 +162,7 @@ export class QueryModel { /** convert the query to a parameter map */ toRouteParam(): {[param: string]: any} { - const queryTextParams = { query: this.queryText || null }; + const queryTextParams = { query: this.queryText || null }; const searchFieldsParams = { fields: this.searchFields?.map(f => f.name).join(',') || null}; const sortParams = sortSettingsToParams(this.sortBy, this.sortDirection); const highlightParams = { highlight: this.highlightSize || null }; @@ -177,10 +177,8 @@ export class QueryModel { }; } - /** convert this link to an array describing the route, which can be used to create a routerlink */ - toRoute(): any[] { - const params = omitNullParameters(this.toRouteParam()); - return ['/search', this.corpus.name, params]; + toQueryParams() { + return omitNullParameters(this.toRouteParam()); } /** convert the query to an elasticsearch query */ diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index c0812e0fc..9afc9ee11 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -68,12 +68,9 @@ export class SearchComponent extends ParamDirective { teardown() { this.user = undefined; this.corpusSubscription.unsubscribe(); - this.setParams( {query: null }); } setStateFromParams(params: ParamMap) { - this.queryText = params.get('query'); - this.queryModel.setFromParams(params); this.tabIndex = params.has('visualize') ? 1 : 0; this.showVisualization = params.has('visualize') ? true : false; } @@ -128,9 +125,12 @@ export class SearchComponent extends ParamDirective { } private setQueryModel() { - this.queryModel = new QueryModel(this.corpus); - this.queryModel.setFromParams(this.route.snapshot.paramMap); + const queryModel = new QueryModel(this.corpus); + queryModel.setFromParams(this.route.snapshot.queryParamMap); + this.queryModel = queryModel; + this.queryText = queryModel.queryText; this.queryModel.update.subscribe(() => { + this.queryText = this.queryModel.queryText || undefined; this.setParams(this.queryModel.toRouteParam()); }); } diff --git a/frontend/src/app/utils/document-context.spec.ts b/frontend/src/app/utils/document-context.spec.ts index 3a69daa85..565ffc9bb 100644 --- a/frontend/src/app/utils/document-context.spec.ts +++ b/frontend/src/app/utils/document-context.spec.ts @@ -17,11 +17,10 @@ describe('document context utils', () => { }; it('should create a document context link', () => { - const link = makeContextParams(document, corpus); - expect(link).toEqual({ + const params = makeContextParams(document, corpus); + expect(params).toEqual({ date: '1900-01-01:1900-01-01', sort: 'ordering,asc' }); - }); }); diff --git a/frontend/src/app/utils/document-context.ts b/frontend/src/app/utils/document-context.ts index 42b1325fe..0b2d455eb 100644 --- a/frontend/src/app/utils/document-context.ts +++ b/frontend/src/app/utils/document-context.ts @@ -19,5 +19,6 @@ const documentContextQuery = (corpus: Corpus, document: FoundDocument): QueryMod export const makeContextParams = (document: FoundDocument, corpus: Corpus): any => { const queryModel = documentContextQuery(corpus, document); - return queryModel.toRouteParam(); + return queryModel.toQueryParams(); }; + From 9cf9936ffc47e5cd59d9d127d0596b3153540b0f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 12:45:01 +0100 Subject: [PATCH 134/262] use queryModel.toroute in search history --- .../app/history/search-history/search-history.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/history/search-history/search-history.component.ts b/frontend/src/app/history/search-history/search-history.component.ts index 2fa22b480..edeb012a5 100644 --- a/frontend/src/app/history/search-history/search-history.component.ts +++ b/frontend/src/app/history/search-history/search-history.component.ts @@ -40,8 +40,8 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { } returnToSavedQuery(query: QueryDb) { - const params = query.queryModel.toRouteParam(); - this.router.navigate(['/search', query.corpus, params]); + this.router.navigate(query.queryModel.toRoute(), + {queryParams: query.queryModel.toQueryParams()}); if (window) { window.scrollTo(0, 0); } From b2d4cc36740b199cebee4fb61a0eb4b2b0792972 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 14:40:50 +0100 Subject: [PATCH 135/262] fix filter descriptions --- frontend/src/app/models/filter-management.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index 8e19f7cc2..1f36978a1 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -8,6 +8,7 @@ import { SearchFilterType } from './search-filter-options'; export class PotentialFilter { filter: SearchFilter; useAsFilter = new BehaviorSubject(false); + description: string; showReset?: boolean; grayedOut?: boolean; adHoc?: boolean; @@ -21,10 +22,13 @@ export class PotentialFilter { } if (!corpusField.filterOptions) { + this.description = `View results from this ${corpusField.displayName}`; this.adHoc = true; } else { + this.description = corpusField.filterOptions.description; this.adHoc = false; } + this.filter.isDefault$.subscribe(this.deactivateWhenDefault.bind(this)); } From e4944876084c3de82b7fa044585186ae09488902 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 15:18:41 +0100 Subject: [PATCH 136/262] make SortConfiguration class --- frontend/src/app/models/query.ts | 37 ++++-------- frontend/src/app/models/sort.ts | 57 ++++++++++++++++++- .../app/search/search-sorting.component.ts | 28 ++++----- frontend/src/app/utils/document-context.ts | 6 +- frontend/src/app/utils/sort.ts | 8 --- 5 files changed, 83 insertions(+), 53 deletions(-) delete mode 100644 frontend/src/app/utils/sort.ts diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 451efd843..bd91d5150 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,12 +1,13 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; import { Subject } from 'rxjs'; -import { Corpus, CorpusField, SortBy, SortDirection } from '../models/index'; +import { Corpus, CorpusField, SortConfiguration, } from '../models/index'; import { EsQuery } from '../services'; -import { combineSearchClauseAndFilters, makeHighlightSpecification, makeSortSpecification } from '../utils/es-query'; -import { filtersFromParams, highlightFromParams, omitNullParameters, queryFiltersToParams, - queryFromParams, searchFieldsFromParams, sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; -import { sortByDefault } from '../utils/sort'; +import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; +import { + filtersFromParams, highlightFromParams, omitNullParameters, queryFiltersToParams, + queryFromParams, searchFieldsFromParams +} from '../utils/params'; import { SearchFilter } from './search-filter'; /** This is the query object as it is saved in the database.*/ @@ -73,25 +74,17 @@ export class QueryModel { queryText: string; searchFields: CorpusField[]; filters: SearchFilter[] = []; - sortBy: SortBy = 'default'; - sortDirection: SortDirection; + sort: SortConfiguration; highlightSize: number; update = new Subject(); constructor(corpus: Corpus) { this.corpus = corpus; + this.sort = new SortConfiguration(this.corpus); + this.sort.configuration$.subscribe(() => this.update.next()); } - /** sort direction to be used in searching: replaces 'default' with the default value */ - get actualSortBy(): CorpusField|'relevance' { - if (this.sortBy !== 'default') { - return this.sortBy; - } else { - return sortByDefault(this.corpus); - } - } - setQueryText(text?: string) { this.queryText = text; this.update.next(); @@ -122,12 +115,6 @@ export class QueryModel { this.update.next(); } - setSort(sortBy: SortBy, sortDirection: SortDirection) { - this.sortBy = sortBy; - this.sortDirection = sortDirection; - this.update.next(); - } - setHighlight(size?: number) { this.highlightSize = size; this.update.next(); @@ -138,7 +125,7 @@ export class QueryModel { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); this.filters = filtersFromParams(params, this.corpus); - [this.sortBy, this.sortDirection] = sortSettingsFromParams(params, this.corpus.fields); + this.sort.setFromParams(params); this.highlightSize = highlightFromParams(params); this.update.next(); } @@ -164,7 +151,7 @@ export class QueryModel { toRouteParam(): {[param: string]: any} { const queryTextParams = { query: this.queryText || null }; const searchFieldsParams = { fields: this.searchFields?.map(f => f.name).join(',') || null}; - const sortParams = sortSettingsToParams(this.sortBy, this.sortDirection); + const sortParams = this.sort.toRouteParam(); const highlightParams = { highlight: this.highlightSize || null }; const filterParams = queryFiltersToParams(this); @@ -186,7 +173,7 @@ export class QueryModel { const filters = this.filters.map(filter => filter.toEsFilter()); const query = combineSearchClauseAndFilters(this.queryText, filters, this.searchFields); - const sort = makeSortSpecification(this.actualSortBy, this.sortDirection); + const sort = this.sort.toEsQuerySort(); const highlight = makeHighlightSpecification(this.corpus, this.queryText, this.highlightSize); return { diff --git a/frontend/src/app/models/sort.ts b/frontend/src/app/models/sort.ts index cfbcd8dac..44a935902 100644 --- a/frontend/src/app/models/sort.ts +++ b/frontend/src/app/models/sort.ts @@ -1,4 +1,59 @@ -import { CorpusField } from './corpus'; +import { ParamMap } from '@angular/router'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { makeSortSpecification } from '../utils/es-query'; +import { sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; +import { Corpus, CorpusField } from './corpus'; export type SortBy = CorpusField | 'relevance' | 'default'; export type SortDirection = 'asc'|'desc'; + +export class SortConfiguration { + sortBy = new BehaviorSubject('default'); + sortDirection = new BehaviorSubject('desc'); + + configuration$ = combineLatest([this.sortBy, this.sortDirection]); + + constructor(private corpus: Corpus) {} + + /** sort direction to be used in searching: replaces 'default' with the default value */ + get actualSortBy(): CorpusField|'relevance' { + return this.sortBy.value !== 'default' ? + this.sortBy.value : this.defaultSortBy; + } + + private get defaultSortBy(): CorpusField | 'relevance' { + return this.corpus.fields.find(field => field.primarySort) || 'relevance'; + + } + + setSortBy(value: SortBy) { + this.sortBy.next(value); + if (value === 'default' || 'relevance') { + this.sortDirection.next('desc'); + } + } + + setSortDirection(value: SortDirection) { + this.sortDirection.next(value); + } + + reset() { + this.sortBy.next('default'); + this.sortDirection.next('desc'); + } + + setFromParams(params: ParamMap) { + const [sortBy, direction] = sortSettingsFromParams(params, this.corpus.fields); + this.sortBy.next(sortBy); + this.sortDirection.next(direction); + } + + toRouteParam(): {sort: string} { + return sortSettingsToParams(this.sortBy.value, this.sortDirection.value); + } + + /** convert this configuration to the 'sort' part of an elasticsearch query */ + toEsQuerySort(): { sort?: any } { + return makeSortSpecification(this.actualSortBy, this.sortDirection.value); + } +} diff --git a/frontend/src/app/search/search-sorting.component.ts b/frontend/src/app/search/search-sorting.component.ts index af9b8821f..837fd3483 100644 --- a/frontend/src/app/search/search-sorting.component.ts +++ b/frontend/src/app/search/search-sorting.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; -import { CorpusField, QueryModel } from '../models'; +import { CorpusField, QueryModel, SortConfiguration } from '../models'; const defaultValueType = 'alpha'; @Component({ @@ -20,6 +20,10 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { constructor() {} + get sortConfiguration(): SortConfiguration { + return this.queryModel.sort; + } + public get sortType(): SortType { return `${this.valueType}${this.ascending ? 'Asc' : 'Desc'}` as SortType; } @@ -33,7 +37,7 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { } ngOnDestroy(): void { - this.queryModel.setSort('default', 'desc'); + this.sortConfiguration.reset(); } setSortableFields() { @@ -42,17 +46,17 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { } setStateFromQueryModel() { - if (this.queryModel.actualSortBy === 'relevance') { + if (this.sortConfiguration.actualSortBy === 'relevance') { this.sortField = undefined; } else { - this.sortField = (this.queryModel.actualSortBy as CorpusField); + this.sortField = (this.sortConfiguration.actualSortBy as CorpusField); } - this.ascending = this.queryModel.sortDirection === 'asc'; + this.ascending = this.sortConfiguration.sortDirection.value === 'asc'; } public toggleSortType() { - this.ascending = !this.ascending; - this.updateSort(); + const direction = this.ascending ? 'desc' : 'asc'; + this.sortConfiguration.setSortDirection(direction); } public toggleShowFields() { @@ -62,18 +66,10 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { public changeField(field: CorpusField | undefined) { if (field === undefined) { this.valueType = defaultValueType; - this.ascending = false; } else { this.valueType = ['integer', 'date', 'boolean'].indexOf(field.displayType) >= 0 ? 'numeric' : 'alpha'; } - this.sortField = field; - this.updateSort(); - } - - private updateSort() { - const sortBy = this.sortField || 'relevance'; - const direction = this.ascending ? 'asc': 'desc'; - this.queryModel.setSort(sortBy, direction); + this.sortConfiguration.setSortBy(field || 'relevance'); } } diff --git a/frontend/src/app/utils/document-context.ts b/frontend/src/app/utils/document-context.ts index 0b2d455eb..dfa0565be 100644 --- a/frontend/src/app/utils/document-context.ts +++ b/frontend/src/app/utils/document-context.ts @@ -1,4 +1,4 @@ -import { Corpus, FoundDocument, QueryModel } from '../models'; +import { Corpus, FoundDocument, QueryModel, SortConfiguration } from '../models'; const documentContextQuery = (corpus: Corpus, document: FoundDocument): QueryModel => { const queryModel = new QueryModel(corpus); @@ -11,8 +11,8 @@ const documentContextQuery = (corpus: Corpus, document: FoundDocument): QueryMod queryModel.addFilter(filter); }); - queryModel.sortBy = spec.sortField || 'default'; - queryModel.sortDirection = spec.sortDirection; + queryModel.sort.sortBy.next(spec.sortField || 'default'); + queryModel.sort.sortDirection.next(spec.sortDirection); return queryModel; }; diff --git a/frontend/src/app/utils/sort.ts b/frontend/src/app/utils/sort.ts deleted file mode 100644 index e6133e091..000000000 --- a/frontend/src/app/utils/sort.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Corpus, CorpusField, SortBy } from '../models'; - -/** get the default sortBy for a corpus */ -export const sortByDefault = (corpus: Corpus): CorpusField|'relevance' => - corpus.fields.find(field => field.primarySort) || 'relevance'; - -export const sortDirectionFromBoolean = (sortAscending: boolean): 'asc'|'desc' => - sortAscending ? 'asc' : 'desc'; From 07611edf194786402e159c8cab6352cd806dd987 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 15:34:59 +0100 Subject: [PATCH 137/262] use undefined instead of null for querytext --- frontend/src/app/models/query.ts | 2 +- frontend/src/app/search/search.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index bd91d5150..f976d6e41 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -86,7 +86,7 @@ export class QueryModel { } setQueryText(text?: string) { - this.queryText = text; + this.queryText = text || undefined; this.update.next(); } diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index 9afc9ee11..739a6d223 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -130,7 +130,7 @@ export class SearchComponent extends ParamDirective { this.queryModel = queryModel; this.queryText = queryModel.queryText; this.queryModel.update.subscribe(() => { - this.queryText = this.queryModel.queryText || undefined; + this.queryText = this.queryModel.queryText; this.setParams(this.queryModel.toRouteParam()); }); } From fa0f3166cb32e9f527fb0d07a52b12629f91d04d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 16:02:27 +0100 Subject: [PATCH 138/262] update wordcloud logic --- .../wordcloud/wordcloud.component.ts | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/frontend/src/app/visualization/wordcloud/wordcloud.component.ts b/frontend/src/app/visualization/wordcloud/wordcloud.component.ts index 62b5a8efa..db9a88fc7 100644 --- a/frontend/src/app/visualization/wordcloud/wordcloud.component.ts +++ b/frontend/src/app/visualization/wordcloud/wordcloud.component.ts @@ -1,13 +1,16 @@ -import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, - ViewChild, ViewEncapsulation } from '@angular/core'; +import { + Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, + ViewChild, ViewEncapsulation +} from '@angular/core'; import * as cloud from 'd3-cloud'; import * as d3 from 'd3'; import { AggregateResult, CorpusField, QueryModel, Corpus, FreqTableHeaders } from '../../models/index'; -import { DialogService, SearchService, ApiService } from '../../services/index'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { ApiService } from '../../services/index'; +import { BehaviorSubject } from 'rxjs'; import { VisualizationService } from '../../services/visualization.service'; +import { showLoading } from '../../utils/utils'; @Component({ selector: 'ia-wordcloud', @@ -33,7 +36,6 @@ export class WordcloudComponent implements OnChanges, OnInit, OnDestroy { { key: 'doc_count', label: 'Frequency' } ]; - public significantText: AggregateResult[]; public disableLoadMore = false; private tasksToCancel: string[] = []; @@ -49,6 +51,10 @@ export class WordcloudComponent implements OnChanges, OnInit, OnDestroy { constructor(private visualizationService: VisualizationService, private apiService: ApiService) { } + get readyToLoad() { + return (this.corpus && this.visualizedField && this.queryModel && this.palette); + } + ngOnInit() { if (this.resultsCount > 0) { this.disableLoadMore = this.resultsCount < this.batchSize; @@ -60,35 +66,37 @@ export class WordcloudComponent implements OnChanges, OnInit, OnDestroy { } ngOnChanges(changes: SimpleChanges) { - if ((this.corpus && this.visualizedField && this.queryModel && this.batchSize && this.palette) && - (changes.corpus || changes.visualizedField || changes.queryModel || changes.batchSize)) { - this.loadData(this.batchSize); + if (this.readyToLoad && + (changes.corpus || changes.visualizedField || changes.queryModel)) { + if (changes.queryModel) { + this.queryModel.update.subscribe(this.loadData.bind(this)); + } + this.loadData(); } else { - this.onDataLoaded(); + this.makeChart(); } } - loadData(size: number = null) { - this.isLoading.next(true); - this.visualizationService.getWordcloudData(this.visualizedField.name, this.queryModel, this.corpus.name, size).then(result => { - this.significantText = result; - this.onDataLoaded(); - }) - .catch(this.emitError.bind(this)); + loadData() { + showLoading( + this.isLoading, + this.visualizationService.getWordcloudData( + this.visualizedField.name, this.queryModel, this.corpus.name, this.batchSize + ).then(this.onDataLoaded.bind(this)).catch(this.emitError.bind(this)) + ); } loadMoreData() { - this.isLoading.next(true); - const queryModel = this.queryModel; - if (queryModel) { - this.visualizationService.getWordcloudTasks(this.visualizedField.name, queryModel, this.corpus.name).then(response => { - this.tasksToCancel = response; - this.apiService.pollTasks(response).then( outcome => { - const result = outcome[0]; - this.significantText = result; - this.onDataLoaded(); - }); - }).catch(this.emitError.bind(this)); + if (this.readyToLoad) { + showLoading( + this.isLoading, + this.visualizationService.getWordcloudTasks(this.visualizedField.name, this.queryModel, this.corpus.name).then(response => { + this.tasksToCancel = response; + return this.apiService.pollTasks(response).then( outcome => + this.onDataLoaded(outcome[0]) + ); + }).catch(this.emitError.bind(this)) + ); } } @@ -96,8 +104,12 @@ export class WordcloudComponent implements OnChanges, OnInit, OnDestroy { this.error.emit(error.message); } - onDataLoaded() { - this.isLoading.next(false); + onDataLoaded(result: AggregateResult[]) { + this.significantText = result; + this.makeChart(); + } + + makeChart() { this.chartElement = this.chartContainer.nativeElement; d3.select('svg.wordcloud').remove(); const inputRange = d3.extent(this.significantText.map(d => d.doc_count)) as number[]; From eb19cb8e74a9b33f6741f04c283496c065e31c33 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 16:18:16 +0100 Subject: [PATCH 139/262] improve filter subscription logic --- frontend/src/app/models/query.spec.ts | 37 ++++++++++++++++++- frontend/src/app/models/query.ts | 34 ++++++++++++----- .../barchart/barchart.directive.ts | 1 + 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index a18165eae..c95665568 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -1,7 +1,7 @@ -import { mockField2, mockFieldDate } from '../../mock-data/corpus'; +import { mockField2, mockFieldDate, mockFieldMultipleChoice } from '../../mock-data/corpus'; import { Corpus, } from './corpus'; import { QueryModel } from './query'; -import { DateFilter, SearchFilter } from './search-filter'; +import { DateFilter, MultipleChoiceFilter, SearchFilter } from './search-filter'; import { convertToParamMap } from '@angular/router'; const corpus: Corpus = { @@ -19,12 +19,14 @@ const corpus: Corpus = { fields: [ mockField2, mockFieldDate, + mockFieldMultipleChoice, ], }; describe('QueryModel', () => { let query: QueryModel; let filter: SearchFilter; + let filter2: SearchFilter; beforeEach(() => { query = new QueryModel(corpus); @@ -34,6 +36,8 @@ describe('QueryModel', () => { filter = new DateFilter(mockFieldDate); filter.setToValue(new Date('Jan 1 1850')); + filter2 = new MultipleChoiceFilter(mockFieldMultipleChoice); + filter2.setToValue(['hooray!']); }); it('should create', () => { @@ -55,6 +59,31 @@ describe('QueryModel', () => { }); + it('should remove filters', () => { + let updates = 0; + query.update.subscribe(() => updates += 1); + + query.addFilter(filter); + query.addFilter(filter2); + + expect(query.filters.length).toBe(2); + expect(updates).toBe(2); + + filter.setToValue(new Date('Jan 1 1860')); + + expect(updates).toBe(3); + + query.removeFilter(filter); + + expect(query.filters.length).toBe(1); + expect(updates).toBe(4); + + filter.setToValue(new Date('Jan 1 1870')); + + expect(updates).toBe(4); + + }); + it('should convert to an elasticsearch query', () => { expect(query.toEsQuery()).toEqual({ query: { @@ -86,6 +115,7 @@ describe('QueryModel', () => { fields: null, speech: null, date: null, + greater_field: null, sort: null, highlight: null }); @@ -97,6 +127,7 @@ describe('QueryModel', () => { fields: null, speech: null, date: null, + greater_field: null, sort: null, highlight: null, }); @@ -108,6 +139,7 @@ describe('QueryModel', () => { fields: null, speech: null, date: '1850-01-01:1850-01-01', + greater_field: null, sort: null, highlight: null, }); @@ -120,6 +152,7 @@ describe('QueryModel', () => { fields: null, speech: null, date: null, + greater_field: null, sort: null, highlight: null }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index f976d6e41..7e0143516 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,6 +1,6 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; -import { Subject } from 'rxjs'; +import { combineLatest, Subject, Subscription } from 'rxjs'; import { Corpus, CorpusField, SortConfiguration, } from '../models/index'; import { EsQuery } from '../services'; import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; @@ -79,6 +79,8 @@ export class QueryModel { update = new Subject(); + private filterSubscription: Subscription; + constructor(corpus: Corpus) { this.corpus = corpus; this.sort = new SortConfiguration(this.corpus); @@ -92,9 +94,7 @@ export class QueryModel { addFilter(filter: SearchFilter) { this.filters.push(filter); - filter.data.subscribe(data => { - this.update.next(); - }); + this.subscribeToFilterUpdates(); } removeFilter(filter: SearchFilter) { @@ -108,11 +108,12 @@ export class QueryModel { /** remove all filters that apply to a corpus field */ removeFiltersForField(field: CorpusField) { - const filterIndex = () => this.filters.findIndex(filter => filter.corpusField.name === field.name); - while (filterIndex() !== -1) { - this.filters.splice(filterIndex()); + if (this.filterForField(field)) { + _.remove(this.filters, + filter => filter.corpusField.name === field.name + ); + this.subscribeToFilterUpdates(); } - this.update.next(); } setHighlight(size?: number) { @@ -124,7 +125,7 @@ export class QueryModel { setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); - this.filters = filtersFromParams(params, this.corpus); + filtersFromParams(params, this.corpus).forEach(filter => this.addFilter(filter)); this.sort.setFromParams(params); this.highlightSize = highlightFromParams(params); this.update.next(); @@ -180,4 +181,19 @@ export class QueryModel { ...query, ...sort, ...highlight }; } + + private subscribeToFilterUpdates() { + if (this.filterSubscription) { + this.filterSubscription.unsubscribe(); + } + if (this.filters.length) { + this.filterSubscription = combineLatest( + this.filters.map(f => f.data) + ).subscribe(() => this.update.next()); + } else { + this.filterSubscription = undefined; + this.update.next(); + } + } + } diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts index b8c680f63..00dc15a58 100644 --- a/frontend/src/app/visualization/barchart/barchart.directive.ts +++ b/frontend/src/app/visualization/barchart/barchart.directive.ts @@ -414,6 +414,7 @@ export abstract class BarchartDirective getSeriesDocumentData( series: BarchartSeries, queryModel: QueryModel = this.queryModel, setSearchRatio = true ): Promise> { + console.log(queryModel); const queryModelCopy = this.queryModelForSeries(series, queryModel); return this.requestSeriesDocCounts(queryModelCopy).then(result => From 4a51003044e8b83d8607bd0fc1f36fb45659b51c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 17:41:36 +0100 Subject: [PATCH 140/262] clean up filter widget code --- frontend/src/app/filter/ad-hoc-filter.component.ts | 3 --- frontend/src/app/filter/base-filter.component.ts | 3 +-- frontend/src/app/filter/boolean-filter.component.ts | 3 --- .../src/app/filter/multiple-choice-filter.component.spec.ts | 1 - 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/frontend/src/app/filter/ad-hoc-filter.component.ts b/frontend/src/app/filter/ad-hoc-filter.component.ts index 353a29513..06384741c 100644 --- a/frontend/src/app/filter/ad-hoc-filter.component.ts +++ b/frontend/src/app/filter/ad-hoc-filter.component.ts @@ -8,7 +8,4 @@ import { BaseFilterComponent } from './base-filter.component'; styleUrls: ['./ad-hoc-filter.component.scss'] }) export class AdHocFilterComponent extends BaseFilterComponent { - - onFilterSet(filter: AdHocFilter) {} - } diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 3f5728753..099e8f293 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -40,6 +40,5 @@ export abstract class BaseFilterComponent { } /** possible administration when the filter is set, e.g. setting data limits */ - abstract onFilterSet(filter): void; - + onFilterSet(filter): void {}; } diff --git a/frontend/src/app/filter/boolean-filter.component.ts b/frontend/src/app/filter/boolean-filter.component.ts index 7042e1a1f..3467da463 100644 --- a/frontend/src/app/filter/boolean-filter.component.ts +++ b/frontend/src/app/filter/boolean-filter.component.ts @@ -9,7 +9,4 @@ import { BooleanFilter } from '../models'; styleUrls: ['./boolean-filter.component.scss'] }) export class BooleanFilterComponent extends BaseFilterComponent { - - onFilterSet(filter: BooleanFilter) {} - } diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index 1037d6648..1220467e1 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -17,7 +17,6 @@ describe('MultipleChoiceFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MultipleChoiceFilterComponent); component = fixture.componentInstance; - // component.options = [{value: 'Andy', label: 'Andy', doc_count: 2}, {value: 'Lou', label: 'Lou', doc_count: 3}]; const query = new QueryModel(mockCorpus); component.filter = new PotentialFilter(mockFieldMultipleChoice, query); fixture.detectChanges(); From c9644104bdeb33e1f111de27c339e522a1435ebe Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 17:48:04 +0100 Subject: [PATCH 141/262] simplify grayedout logic --- frontend/src/app/filter/base-filter.component.ts | 2 -- frontend/src/app/filter/boolean-filter.component.html | 2 +- frontend/src/app/filter/date-filter.component.html | 4 ++-- frontend/src/app/filter/filter-manager.component.html | 8 ++++---- frontend/src/app/filter/filter-manager.component.ts | 2 -- .../src/app/filter/multiple-choice-filter.component.html | 2 +- .../src/app/filter/multiple-choice-filter.component.ts | 2 +- frontend/src/app/filter/range-filter.component.html | 2 +- frontend/src/app/models/filter-management.ts | 2 -- 9 files changed, 10 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 099e8f293..41a8834ca 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -12,8 +12,6 @@ import { PotentialFilter } from '../models/index'; template: '' }) export abstract class BaseFilterComponent { - @Input() grayedOut: boolean; - private _filter: PotentialFilter; constructor() { } diff --git a/frontend/src/app/filter/boolean-filter.component.html b/frontend/src/app/filter/boolean-filter.component.html index 63925cbbc..7e6a49c60 100644 --- a/frontend/src/app/filter/boolean-filter.component.html +++ b/frontend/src/app/filter/boolean-filter.component.html @@ -1,4 +1,4 @@
- + {{data | json | titlecase }}
diff --git a/frontend/src/app/filter/date-filter.component.html b/frontend/src/app/filter/date-filter.component.html index d656d84ab..708351ec9 100644 --- a/frontend/src/app/filter/date-filter.component.html +++ b/frontend/src/app/filter/date-filter.component.html @@ -1,8 +1,8 @@
- -
diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index 6a66c39ac..ae58c2f9e 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -44,10 +44,10 @@

Filters

- - - - + + + +
diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 40ea343aa..72d8b06e2 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -33,8 +33,6 @@ export class FilterManagerComponent { public potentialFilters: PotentialFilter[] = []; - public grayOutFilters: boolean; - private _corpus: Corpus; private _queryModel: QueryModel; diff --git a/frontend/src/app/filter/multiple-choice-filter.component.html b/frontend/src/app/filter/multiple-choice-filter.component.html index 803ca1165..7fb112711 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.html +++ b/frontend/src/app/filter/multiple-choice-filter.component.html @@ -1,5 +1,5 @@
-
{{item.label}}
diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index e671c59d5..3ca994e14 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -33,6 +33,6 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent aggregations.map(x => ({ label: x.key, value: encodeURIComponent(x.key), doc_count: x.doc_count })), o => o.label ) - ); + ).catch(() => this.options = []); } } diff --git a/frontend/src/app/filter/range-filter.component.html b/frontend/src/app/filter/range-filter.component.html index f3089867e..ba498e929 100644 --- a/frontend/src/app/filter/range-filter.component.html +++ b/frontend/src/app/filter/range-filter.component.html @@ -1,5 +1,5 @@
{{data.min}} - {{data.max}} -
diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index 1f36978a1..a21ceffb3 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -9,8 +9,6 @@ export class PotentialFilter { filter: SearchFilter; useAsFilter = new BehaviorSubject(false); description: string; - showReset?: boolean; - grayedOut?: boolean; adHoc?: boolean; constructor(public corpusField: CorpusField, public queryModel: QueryModel) { From ffc1c5de528afe0060893edb7ef30b6a821a9bfb Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 20 Mar 2023 18:36:10 +0100 Subject: [PATCH 142/262] fix reference to obsolete variable --- frontend/src/app/select-field/select-field.component.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/select-field/select-field.component.html b/frontend/src/app/select-field/select-field.component.html index 2ff24ab17..078bf63d5 100644 --- a/frontend/src/app/select-field/select-field.component.html +++ b/frontend/src/app/select-field/select-field.component.html @@ -1,6 +1,11 @@ + [(ngModel)]="selectedFields" + optionLabel="displayName" + [displaySelectedLabel]="true" + (onChange)="onUpdate()" + [ngModelOptions]="{standalone: true}" + dropdownIcon="fa fa-cog">
{{allVisible? 'Show default fields' : 'Show all fields'}} From fa1b1e5571794e965cbf22b4998a6ad14cc94561 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 31 Mar 2023 12:12:21 +0200 Subject: [PATCH 143/262] fix empty filter list in es_query -> query model --- backend/api/tests/test_query_model_to_es_query.py | 15 ++++++++++++++- backend/visualization/query.py | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/api/tests/test_query_model_to_es_query.py b/backend/api/tests/test_query_model_to_es_query.py index bca59fa36..86dafd8a6 100644 --- a/backend/api/tests/test_query_model_to_es_query.py +++ b/backend/api/tests/test_query_model_to_es_query.py @@ -50,7 +50,20 @@ def test_query_model_to_es_query(name, query_model, es_query): result = query_model_to_es_query(query_model) assert result == es_query -@pytest.mark.parametrize('name,query_model,es_query', cases, ids=map(get_name, cases)) +extra_cases_for_reverse_op = [ + ( + 'es query without filters / boolean logic', + {'queryText': None, 'filters': [], 'sortAscending': True}, + {'query': {'match_all': {}}} + # should be able to parse an es query without bool structure + # this structur is not generated by the query model -> es query conversion + # but IS sometimes generated by the frontend + ), +] + +reverse_cases = cases + extra_cases_for_reverse_op + +@pytest.mark.parametrize('name,query_model,es_query', reverse_cases, ids=map(get_name, reverse_cases)) def test_es_query_to_query_model(name, query_model, es_query): result = es_query_to_query_model(es_query) diff --git a/backend/visualization/query.py b/backend/visualization/query.py index 790463d7c..aa7d2e306 100644 --- a/backend/visualization/query.py +++ b/backend/visualization/query.py @@ -66,11 +66,11 @@ def set_search_fields(query, fields): return query def get_filters(query): - """Get the list of filters in a query, or `None` if there are none.""" + """Get the list of filters in a query. Returns an empty list if there are none.""" try: filters = query['query']['bool']['filter'] except KeyError: - filters = None + filters = [] return filters From eec8fbf4ca917308a43166055742d7db11bf949b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 12 Apr 2023 17:07:15 +0200 Subject: [PATCH 144/262] merge migrations --- backend/api/migrations/0003_merge_20230412_1706.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 backend/api/migrations/0003_merge_20230412_1706.py diff --git a/backend/api/migrations/0003_merge_20230412_1706.py b/backend/api/migrations/0003_merge_20230412_1706.py new file mode 100644 index 000000000..4ac979956 --- /dev/null +++ b/backend/api/migrations/0003_merge_20230412_1706.py @@ -0,0 +1,14 @@ +# Generated by Django 4.1.5 on 2023-04-12 15:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_alter_query_started'), + ('api', '0002_convert_to_es_query'), + ] + + operations = [ + ] From 24a3671db3115a94d6e3967d18be3eaba606addf Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 24 May 2023 12:00:20 +0200 Subject: [PATCH 145/262] correct query link in search history --- .../src/app/history/search-history/search-history.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/history/search-history/search-history.component.ts b/frontend/src/app/history/search-history/search-history.component.ts index edeb012a5..7e6addc15 100644 --- a/frontend/src/app/history/search-history/search-history.component.ts +++ b/frontend/src/app/history/search-history/search-history.component.ts @@ -40,7 +40,7 @@ export class SearchHistoryComponent extends HistoryDirective implements OnInit { } returnToSavedQuery(query: QueryDb) { - this.router.navigate(query.queryModel.toRoute(), + this.router.navigate(['/search', query.corpus], {queryParams: query.queryModel.toQueryParams()}); if (window) { window.scrollTo(0, 0); From 92c4a880437b33abd40442c75e3dfc031f9da44c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 24 May 2023 12:13:25 +0200 Subject: [PATCH 146/262] adapt to upstream changes in utils --- frontend/src/app/models/sort.ts | 2 +- frontend/src/app/utils/es-query.spec.ts | 11 +++++------ frontend/src/app/utils/params.spec.ts | 9 +++++---- frontend/src/app/utils/params.ts | 4 ++-- frontend/src/mock-data/search.ts | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/models/sort.ts b/frontend/src/app/models/sort.ts index 44a935902..f1c75729a 100644 --- a/frontend/src/app/models/sort.ts +++ b/frontend/src/app/models/sort.ts @@ -48,7 +48,7 @@ export class SortConfiguration { this.sortDirection.next(direction); } - toRouteParam(): {sort: string} { + toRouteParam(): {sort: string|null} { return sortSettingsToParams(this.sortBy.value, this.sortDirection.value); } diff --git a/frontend/src/app/utils/es-query.spec.ts b/frontend/src/app/utils/es-query.spec.ts index 994331d12..13c66c288 100644 --- a/frontend/src/app/utils/es-query.spec.ts +++ b/frontend/src/app/utils/es-query.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { mockField2, mockField3 } from '../../mock-data/corpus'; +import { mockCorpus3, mockField, mockField2 } from '../../mock-data/corpus'; import { makeHighlightSpecification, makeSimpleQueryString, makeSortSpecification } from './es-query'; describe('es-query utils', () => { @@ -15,17 +15,16 @@ describe('es-query utils', () => { }); it('should make a sort specification', () => { - expect(makeSortSpecification(undefined, true)).toEqual({}); - expect(makeSortSpecification('great_field', false)).toEqual({ + expect(makeSortSpecification('relevance', 'asc')).toEqual({}); + expect(makeSortSpecification(mockField, 'desc')).toEqual({ sort: [{ great_field: 'desc' }] }); }); it('should make a highlight specification', () => { - const fields = [mockField2, mockField3]; - expect(makeHighlightSpecification(fields, 'test', undefined)).toEqual({}); + expect(makeHighlightSpecification(mockCorpus3, 'test', undefined)).toEqual({}); - expect(makeHighlightSpecification(fields, 'test', 100)).toEqual({ + expect(makeHighlightSpecification(mockCorpus3, 'test', 100)).toEqual({ highlight: { fragment_size: 100, pre_tags: [''], diff --git a/frontend/src/app/utils/params.spec.ts b/frontend/src/app/utils/params.spec.ts index 2e74b2291..2f4630e2b 100644 --- a/frontend/src/app/utils/params.spec.ts +++ b/frontend/src/app/utils/params.spec.ts @@ -1,13 +1,14 @@ import { convertToParamMap } from '@angular/router'; import { highlightFromParams, omitNullParameters, searchFieldsFromParams } from './params'; -import { omitNullParameters } from './params'; +import { mockCorpus3, mockField2 } from '../../mock-data/corpus'; describe('searchFieldsFromParams', () => { it('should parse field parameters', () => { - const params = convertToParamMap({fields: 'speech,speaker'}); - const fields = searchFieldsFromParams(params); + const params = convertToParamMap({fields: 'speech,great_field'}); + const corpus = mockCorpus3; + const fields = searchFieldsFromParams(params, corpus); expect(fields.length).toEqual(2); - expect(fields).toContain('speech'); + expect(fields).toContain(mockField2); }); }); diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 74df1cf5d..2a445485c 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -24,10 +24,10 @@ export const highlightFromParams = (params: ParamMap): number => // sort -export const sortSettingsToParams = (sortBy: SortBy, direction: SortDirection): {sort?: string} => { +export const sortSettingsToParams = (sortBy: SortBy, direction: SortDirection): {sort: string|null} => { let sortByName: string; if (sortBy === 'default') { - return {}; + return { sort: null }; } else if (sortBy === 'relevance') { sortByName = sortBy; } else { diff --git a/frontend/src/mock-data/search.ts b/frontend/src/mock-data/search.ts index 57754e15e..af9b0406d 100644 --- a/frontend/src/mock-data/search.ts +++ b/frontend/src/mock-data/search.ts @@ -34,8 +34,8 @@ export class SearchServiceMock { filters.forEach(model.addFilter); if (sortField) { - model.sortBy = sortField; - model.sortDirection = sortAscending ? 'asc' : 'desc'; + model.sort.setSortBy(sortField); + model.sort.setSortDirection(sortAscending ? 'asc' : 'desc'); } model.highlightSize = highlight; From 6a6085c04b52ea83d4b0cbb7f8bef6c6350448dd Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 24 May 2023 12:22:25 +0200 Subject: [PATCH 147/262] add querymodel to field select input --- frontend/src/app/search/search.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 63b19a840..5b5674338 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -30,7 +30,7 @@
- +
From 19406cd8a03cecad4428c45b5612b1ca2e0f0bb0 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 23 May 2023 16:25:52 +0200 Subject: [PATCH 148/262] fix querymodel -> esquery conversion - properly search keword/text multifields --- frontend/src/app/utils/es-query.spec.ts | 13 +++++++++++-- frontend/src/app/utils/es-query.ts | 11 +++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/utils/es-query.spec.ts b/frontend/src/app/utils/es-query.spec.ts index 13c66c288..c31e0264e 100644 --- a/frontend/src/app/utils/es-query.spec.ts +++ b/frontend/src/app/utils/es-query.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { mockCorpus3, mockField, mockField2 } from '../../mock-data/corpus'; -import { makeHighlightSpecification, makeSimpleQueryString, makeSortSpecification } from './es-query'; +import { mockField, mockCorpus3, mockField2 } from '../../mock-data/corpus'; +import { makeEsSearchClause, makeHighlightSpecification, makeSimpleQueryString, makeSortSpecification } from './es-query'; describe('es-query utils', () => { it('should make a simple query string clause', () => { @@ -14,6 +14,15 @@ describe('es-query utils', () => { }); }); + it('should set search fields', () => { + const esQuery = makeEsSearchClause('test', [mockField, mockField2]); + expect(esQuery['simple_query_string'].fields).toEqual(['great_field', 'speech']); + + const esQuery2 = makeEsSearchClause('test', [mockField2]); + expect(esQuery2['simple_query_string'].fields).toEqual(['speech']); + + }); + it('should make a sort specification', () => { expect(makeSortSpecification('relevance', 'asc')).toEqual({}); expect(makeSortSpecification(mockField, 'desc')).toEqual({ diff --git a/frontend/src/app/utils/es-query.ts b/frontend/src/app/utils/es-query.ts index 1143bbee1..fa5d706db 100644 --- a/frontend/src/app/utils/es-query.ts +++ b/frontend/src/app/utils/es-query.ts @@ -23,12 +23,19 @@ export const makeSimpleQueryString = (queryText: string, searchFields?: CorpusFi } }; if (searchFields) { - const fieldNames = searchFields.map(field => field.name); - _.set(clause, 'simple_query_string.fields', fieldNames); + _.set(clause, 'simple_query_string.fields', searchFields.map(searchFieldName)); } return clause; }; +const searchFieldName = (field: CorpusField): string => { + if (field.multiFields?.includes('text')) { + return `${field.name}.text`; + } else { + return field.name; + } +}; + export const makeEsSearchClause = (queryText?: string, searchFields?: CorpusField[]): EsSearchClause => { if (queryText) { return makeSimpleQueryString(queryText, searchFields); From a2090771b792cc763da37a66643ad35aa168edfe Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 23 May 2023 16:43:35 +0200 Subject: [PATCH 149/262] add multifield unit test --- frontend/src/app/utils/es-query.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/app/utils/es-query.spec.ts b/frontend/src/app/utils/es-query.spec.ts index c31e0264e..9f6e3238c 100644 --- a/frontend/src/app/utils/es-query.spec.ts +++ b/frontend/src/app/utils/es-query.spec.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import * as _ from 'lodash'; import { mockField, mockCorpus3, mockField2 } from '../../mock-data/corpus'; import { makeEsSearchClause, makeHighlightSpecification, makeSimpleQueryString, makeSortSpecification } from './es-query'; @@ -21,6 +22,11 @@ describe('es-query utils', () => { const esQuery2 = makeEsSearchClause('test', [mockField2]); expect(esQuery2['simple_query_string'].fields).toEqual(['speech']); + + const multifield = _.set(_.clone(mockField), 'multiFields', ['text']); + const esQuery3 = makeEsSearchClause('test', [multifield, mockField2]); + expect(esQuery3['simple_query_string'].fields).toEqual(['great_field.text', 'speech']); + }); it('should make a sort specification', () => { From fad2639b60a4d5e022bd6fb07d23aecede2453ac Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 24 May 2023 13:26:02 +0200 Subject: [PATCH 150/262] fix uri encoding for multiple choice data --- .../app/filter/multiple-choice-filter.component.ts | 2 +- frontend/src/app/models/search-filter.spec.ts | 12 ++++++++---- frontend/src/app/models/search-filter.ts | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index 3ca994e14..d936ae569 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -30,7 +30,7 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent this.searchService.aggregateSearch(queryModel.corpus, queryModel, [aggregator]).then( response => response.aggregations[this.filter.corpusField.name]).then(aggregations => this.options = _.sortBy( - aggregations.map(x => ({ label: x.key, value: encodeURIComponent(x.key), doc_count: x.doc_count })), + aggregations.map(x => ({ label: x.key, value: x.key, doc_count: x.doc_count })), o => o.label ) ).catch(() => this.options = []); diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index 1d04ec8a8..e4f08f3d1 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -65,15 +65,19 @@ describe('MultipleChoiceFilter', () => { expect(filter.currentData).toEqual([]); }); - it('should convert to string', () => { + it('should convert to a string', () => { expect(filter.dataFromString(filter.dataToString(filter.currentData))) .toEqual(filter.currentData); // non-empty value - filter.data.next(['a', 'b']); + filter.data.next(['a', 'b', 'value with spaces']); expect(filter.dataFromString(filter.dataToString(filter.currentData))) .toEqual(filter.currentData); + }); + it('should convert values to valid URI components', () => { + filter.data.next(['a long value']); + expect(filter.dataToString(filter.currentData)).not.toContain(' '); }); it('should set data from a value', () => { @@ -83,11 +87,11 @@ describe('MultipleChoiceFilter', () => { }); it('should convert to an elasticsearch filter', () => { - filter.data.next(['wow!']); + filter.data.next(['wow!', 'a great selection!']); const esFilter = filter.toEsFilter(); expect(esFilter).toEqual({ terms: { - greater_field: ['wow!'] + greater_field: ['wow!', 'a great selection!'] } }); }); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index eb1a2e7e4..1822f7eb3 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -176,13 +176,13 @@ export class MultipleChoiceFilter extends AbstractSearchFilter Date: Wed, 24 May 2023 13:44:37 +0200 Subject: [PATCH 151/262] cleaner logic for sorting --- frontend/src/app/models/sort.spec.ts | 34 ++++++++++++ frontend/src/app/models/sort.ts | 54 ++++++++++++------- .../app/search/search-sorting.component.ts | 8 +-- frontend/src/app/utils/document-context.ts | 4 +- frontend/src/app/utils/es-query.spec.ts | 2 +- frontend/src/app/utils/es-query.ts | 6 +-- frontend/src/app/utils/params.ts | 25 ++------- 7 files changed, 81 insertions(+), 52 deletions(-) create mode 100644 frontend/src/app/models/sort.spec.ts diff --git a/frontend/src/app/models/sort.spec.ts b/frontend/src/app/models/sort.spec.ts new file mode 100644 index 000000000..a2476556a --- /dev/null +++ b/frontend/src/app/models/sort.spec.ts @@ -0,0 +1,34 @@ +import { convertToParamMap } from '@angular/router'; +import { mockCorpus3, mockField3 } from '../../mock-data/corpus'; +import { SortConfiguration } from './sort'; + +describe('SortConfiguration', () => { + let sort: SortConfiguration; + + beforeEach(() => { + sort = new SortConfiguration(mockCorpus3); + }); + + it('should set the default state', () => { + expect(sort.sortBy.value).toBe(undefined); + expect(sort.sortDirection.value).toBe('desc'); + expect(sort.isDefault).toBe(true); + }); + + it('should convert to parameters', () => { + sort.setSortBy(mockField3); + sort.setSortDirection('asc'); + + const param = sort.toRouteParam(); + + // set the values to something else... + sort.setSortBy(undefined); + sort.setSortDirection('desc'); + + // now restore them from the parameter + sort.setFromParams(convertToParamMap(param)); + + expect(sort.sortBy.value).toEqual(mockField3); + expect(sort.sortDirection.value).toBe('asc'); + }); +}); diff --git a/frontend/src/app/models/sort.ts b/frontend/src/app/models/sort.ts index f1c75729a..916e12d60 100644 --- a/frontend/src/app/models/sort.ts +++ b/frontend/src/app/models/sort.ts @@ -1,34 +1,40 @@ import { ParamMap } from '@angular/router'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { makeSortSpecification } from '../utils/es-query'; -import { sortSettingsFromParams, sortSettingsToParams } from '../utils/params'; +import { sortSettingsToParams } from '../utils/params'; import { Corpus, CorpusField } from './corpus'; +import * as _ from 'lodash'; +import { findByName } from '../utils/utils'; -export type SortBy = CorpusField | 'relevance' | 'default'; +export type SortBy = CorpusField | undefined; export type SortDirection = 'asc'|'desc'; export class SortConfiguration { - sortBy = new BehaviorSubject('default'); + sortBy = new BehaviorSubject(undefined); sortDirection = new BehaviorSubject('desc'); configuration$ = combineLatest([this.sortBy, this.sortDirection]); - constructor(private corpus: Corpus) {} + private defaultSortBy: SortBy; + private defaultSortDirection: SortDirection = 'desc'; - /** sort direction to be used in searching: replaces 'default' with the default value */ - get actualSortBy(): CorpusField|'relevance' { - return this.sortBy.value !== 'default' ? - this.sortBy.value : this.defaultSortBy; + constructor(private corpus: Corpus) { + this.defaultSortBy = this.corpus.fields.find(field => field.primarySort); + this.sortBy.next(this.defaultSortBy); } - private get defaultSortBy(): CorpusField | 'relevance' { - return this.corpus.fields.find(field => field.primarySort) || 'relevance'; - + /** + * Whether the current state is the default sorting state + */ + get isDefault(): boolean { + return _.isEqual(this.sortBy.value, this.defaultSortBy) && this.sortDirection.value === this.defaultSortDirection; } setSortBy(value: SortBy) { this.sortBy.next(value); - if (value === 'default' || 'relevance') { + + // sorting by relevance is always descending + if (!value) { this.sortDirection.next('desc'); } } @@ -38,22 +44,34 @@ export class SortConfiguration { } reset() { - this.sortBy.next('default'); - this.sortDirection.next('desc'); + this.sortBy.next(this.defaultSortBy); + this.sortDirection.next(this.defaultSortDirection); } setFromParams(params: ParamMap) { - const [sortBy, direction] = sortSettingsFromParams(params, this.corpus.fields); - this.sortBy.next(sortBy); - this.sortDirection.next(direction); + if (params.has('sort')) { + const [sortParam, ascParam] = params.get('sort').split(','); + if ( sortParam === 'relevance' ) { + this.sortBy.next(undefined); + } else { + const field = findByName(this.corpus.fields, sortParam); + this.sortBy.next(field); + } + this.setSortDirection(ascParam as 'asc'|'desc'); + } else { + this.reset(); + } } toRouteParam(): {sort: string|null} { + if (this.isDefault) { + return {sort: null}; + } return sortSettingsToParams(this.sortBy.value, this.sortDirection.value); } /** convert this configuration to the 'sort' part of an elasticsearch query */ toEsQuerySort(): { sort?: any } { - return makeSortSpecification(this.actualSortBy, this.sortDirection.value); + return makeSortSpecification(this.sortBy.value, this.sortDirection.value); } } diff --git a/frontend/src/app/search/search-sorting.component.ts b/frontend/src/app/search/search-sorting.component.ts index 837fd3483..a23024314 100644 --- a/frontend/src/app/search/search-sorting.component.ts +++ b/frontend/src/app/search/search-sorting.component.ts @@ -46,11 +46,7 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { } setStateFromQueryModel() { - if (this.sortConfiguration.actualSortBy === 'relevance') { - this.sortField = undefined; - } else { - this.sortField = (this.sortConfiguration.actualSortBy as CorpusField); - } + this.sortField = this.sortConfiguration.sortBy.value; this.ascending = this.sortConfiguration.sortDirection.value === 'asc'; } @@ -69,7 +65,7 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { } else { this.valueType = ['integer', 'date', 'boolean'].indexOf(field.displayType) >= 0 ? 'numeric' : 'alpha'; } - this.sortConfiguration.setSortBy(field || 'relevance'); + this.sortConfiguration.setSortBy(field || undefined); } } diff --git a/frontend/src/app/utils/document-context.ts b/frontend/src/app/utils/document-context.ts index dfa0565be..160cbc88f 100644 --- a/frontend/src/app/utils/document-context.ts +++ b/frontend/src/app/utils/document-context.ts @@ -1,4 +1,4 @@ -import { Corpus, FoundDocument, QueryModel, SortConfiguration } from '../models'; +import { Corpus, FoundDocument, QueryModel } from '../models'; const documentContextQuery = (corpus: Corpus, document: FoundDocument): QueryModel => { const queryModel = new QueryModel(corpus); @@ -11,7 +11,7 @@ const documentContextQuery = (corpus: Corpus, document: FoundDocument): QueryMod queryModel.addFilter(filter); }); - queryModel.sort.sortBy.next(spec.sortField || 'default'); + queryModel.sort.sortBy.next(spec.sortField); queryModel.sort.sortDirection.next(spec.sortDirection); return queryModel; diff --git a/frontend/src/app/utils/es-query.spec.ts b/frontend/src/app/utils/es-query.spec.ts index 9f6e3238c..087cadd4d 100644 --- a/frontend/src/app/utils/es-query.spec.ts +++ b/frontend/src/app/utils/es-query.spec.ts @@ -30,7 +30,7 @@ describe('es-query utils', () => { }); it('should make a sort specification', () => { - expect(makeSortSpecification('relevance', 'asc')).toEqual({}); + expect(makeSortSpecification(undefined, 'asc')).toEqual({}); expect(makeSortSpecification(mockField, 'desc')).toEqual({ sort: [{ great_field: 'desc' }] }); diff --git a/frontend/src/app/utils/es-query.ts b/frontend/src/app/utils/es-query.ts index fa5d706db..e4177ef4e 100644 --- a/frontend/src/app/utils/es-query.ts +++ b/frontend/src/app/utils/es-query.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import { BooleanQuery, Corpus, CorpusField, EsFilter, EsSearchClause, MatchAll, QueryModel, - SimpleQueryString, SortDirection } from '../models'; + SimpleQueryString, SortBy, SortDirection } from '../models'; import { EsQuery } from '../services'; import { findByName } from './utils'; import { SearchFilter } from '../models/search-filter'; @@ -62,8 +62,8 @@ export const combineSearchClauseAndFilters = (queryText: string, filters: EsFilt return { query }; }; -export const makeSortSpecification = (sortBy: CorpusField|'relevance', sortDirection: SortDirection) => { - if (sortBy === 'relevance') { +export const makeSortSpecification = (sortBy: SortBy, sortDirection: SortDirection) => { + if (!sortBy) { return {}; } else { const sortByField = { diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 2a445485c..9d6ee3da7 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -1,7 +1,6 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; import { Corpus, CorpusField, QueryModel, SearchFilter, SortBy, SortDirection } from '../models'; -import { findByName } from './utils'; /** omit keys that mapp to null */ export const omitNullParameters = (params: {[key: string]: any}): {[key: string]: any} => { @@ -26,33 +25,15 @@ export const highlightFromParams = (params: ParamMap): number => export const sortSettingsToParams = (sortBy: SortBy, direction: SortDirection): {sort: string|null} => { let sortByName: string; - if (sortBy === 'default') { - return { sort: null }; - } else if (sortBy === 'relevance') { - sortByName = sortBy; + if (!sortBy) { + sortByName = 'relevance'; } else { sortByName = sortBy.name; } return { sort: `${sortByName},${direction}` }; }; -export const sortSettingsFromParams = (params: ParamMap, corpusFields: CorpusField[]): [SortBy, SortDirection] => { - let sortBy: SortBy; - let sortAscending = true; - if (params.has('sort')) { - const [sortParam, ascParam] = params.get('sort').split(','); - sortAscending = ascParam === 'asc'; - if ( sortParam === 'relevance' ) { - return [sortParam, sortAscending ? 'asc' : 'desc']; - } - sortBy = findByName(corpusFields, sortParam); - } - return [ - sortBy || 'default', - sortAscending ? 'asc' : 'desc' - ]; -}; - +// filters export const filtersFromParams = (params: ParamMap, corpus: Corpus): SearchFilter[] => { const specifiedFields = corpus.fields.filter(field => params.has(field.name)); From c7ee6792b02e4b1b364d6a0fc1ad34f5403828f7 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 24 May 2023 16:01:02 +0200 Subject: [PATCH 152/262] fix merge issue in flask data tranfer --- backend/ianalyzer/flask_data_transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ianalyzer/flask_data_transfer.py b/backend/ianalyzer/flask_data_transfer.py index ca402c4ad..03a18d58d 100644 --- a/backend/ianalyzer/flask_data_transfer.py +++ b/backend/ianalyzer/flask_data_transfer.py @@ -150,7 +150,7 @@ def save_flask_query(row): # some queries refer to corpus names that no longer exist return - query_model = json.loads(row['query']) + query_model = load_json_value(row['query']) es_query = query_model_to_es_query(query_model) query = Query( id=row['id'], From fd528b3f06788e9d377d65908d800b73383c310a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 14:22:25 +0200 Subject: [PATCH 153/262] scaffold corpus info component --- frontend/src/app/app.module.ts | 7 ++++++ .../corpus-info/corpus-info.component.html | 1 + .../corpus-info/corpus-info.component.scss | 0 .../corpus-info/corpus-info.component.spec.ts | 24 +++++++++++++++++++ .../app/corpus-info/corpus-info.component.ts | 15 ++++++++++++ 5 files changed, 47 insertions(+) create mode 100644 frontend/src/app/corpus-info/corpus-info.component.html create mode 100644 frontend/src/app/corpus-info/corpus-info.component.scss create mode 100644 frontend/src/app/corpus-info/corpus-info.component.spec.ts create mode 100644 frontend/src/app/corpus-info/corpus-info.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 4847864a2..5ce3136f7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -90,6 +90,7 @@ import { VerifyEmailComponent } from './login/verify-email/verify-email.componen import { DocumentPageComponent } from './document-page/document-page.component'; import { CorpusSelectorComponent } from './corpus-selection/corpus-selector/corpus-selector.component'; import { CorpusFilterComponent } from './corpus-selection/corpus-filter/corpus-filter.component'; +import { CorpusInfoComponent } from './corpus-info/corpus-info.component'; export const appRoutes: Routes = [ @@ -103,6 +104,11 @@ export const appRoutes: Routes = [ component: WordModelsComponent, canActivate: [CorpusGuard, LoggedOnGuard], }, + { + path: 'info/:corpus', + component: CorpusInfoComponent, + canActivate: [CorpusGuard, LoggedOnGuard] + }, { path: 'document/:corpus/:id', component: DocumentPageComponent, @@ -173,6 +179,7 @@ export const declarations: any[] = [ BooleanFilterComponent, CorpusFilterComponent, CorpusHeaderComponent, + CorpusInfoComponent, CorpusSelectionComponent, CorpusSelectorComponent, DateFilterComponent, diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html new file mode 100644 index 000000000..47e3784c4 --- /dev/null +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -0,0 +1 @@ +

corpus-info works!

diff --git a/frontend/src/app/corpus-info/corpus-info.component.scss b/frontend/src/app/corpus-info/corpus-info.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/corpus-info/corpus-info.component.spec.ts b/frontend/src/app/corpus-info/corpus-info.component.spec.ts new file mode 100644 index 000000000..7ec500d61 --- /dev/null +++ b/frontend/src/app/corpus-info/corpus-info.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { CorpusInfoComponent } from './corpus-info.component'; +import { commonTestBed } from '../common-test-bed'; + +describe('CorpusInfoComponent', () => { + let component: CorpusInfoComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + commonTestBed().testingModule.compileComponents(); + })); + + + beforeEach(() => { + fixture = TestBed.createComponent(CorpusInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/corpus-info/corpus-info.component.ts b/frontend/src/app/corpus-info/corpus-info.component.ts new file mode 100644 index 000000000..6bac9ab12 --- /dev/null +++ b/frontend/src/app/corpus-info/corpus-info.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'ia-corpus-info', + templateUrl: './corpus-info.component.html', + styleUrls: ['./corpus-info.component.scss'] +}) +export class CorpusInfoComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} From b7dc91f4ecd3aaf55120e4fec4db1bc765abe2fc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 14:55:17 +0200 Subject: [PATCH 154/262] scaffold info page --- .../corpus-header.component.html | 28 ++++++------- .../corpus-header/corpus-header.component.ts | 3 +- .../corpus-info/corpus-info.component.html | 39 ++++++++++++++++++- .../app/corpus-info/corpus-info.component.ts | 28 ++++++++++++- frontend/src/app/services/api.service.ts | 2 +- 5 files changed, 79 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/corpus-header/corpus-header.component.html b/frontend/src/app/corpus-header/corpus-header.component.html index 7a3463acd..6902541c1 100644 --- a/frontend/src/app/corpus-header/corpus-header.component.html +++ b/frontend/src/app/corpus-header/corpus-header.component.html @@ -7,27 +7,12 @@

Search Word models of Document in + About “{{corpus.title}}”

-
- - -
- diff --git a/frontend/src/app/corpus-header/corpus-header.component.ts b/frontend/src/app/corpus-header/corpus-header.component.ts index b2f8630a7..5b7e938dd 100644 --- a/frontend/src/app/corpus-header/corpus-header.component.ts +++ b/frontend/src/app/corpus-header/corpus-header.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { faDiagramProject, faInfoCircle, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { faDiagramProject, faInfo, faInfoCircle, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { Corpus } from '../models'; import { DialogService } from '../services'; @@ -16,6 +16,7 @@ export class CorpusHeaderComponent implements OnChanges, OnInit { searchIcon = faMagnifyingGlass; wordModelsIcon = faDiagramProject; + infoIcon = faInfo; wordModelsPresent: boolean; diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index 47e3784c4..690d871a1 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -1 +1,38 @@ -

corpus-info works!

+ + +
+
+
+
+
+

{{corpus.description}}

+
+
+

Language: {{languages}}

+

Type: {{corpus.category}}

+

Period: {{minYear}}-{{maxYear}}

+
+
+
+
+ {{corpus.title}} +
+
+
+
+
+ +
+ +
diff --git a/frontend/src/app/corpus-info/corpus-info.component.ts b/frontend/src/app/corpus-info/corpus-info.component.ts index 6bac9ab12..bd11d8c81 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.ts +++ b/frontend/src/app/corpus-info/corpus-info.component.ts @@ -1,4 +1,7 @@ import { Component, OnInit } from '@angular/core'; +import { ApiService, CorpusService } from '../services'; +import { Corpus } from '../models'; +import { marked } from 'marked'; @Component({ selector: 'ia-corpus-info', @@ -6,10 +9,33 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./corpus-info.component.scss'] }) export class CorpusInfoComponent implements OnInit { + corpus: Corpus; - constructor() { } + description: string; + + constructor(private corpusService: CorpusService, private apiService: ApiService) { } + + get minYear() { + return this.corpus.minDate.getFullYear(); + } + + get maxYear() { + return this.corpus.maxDate.getFullYear(); + } + + get languages() { + return this.corpus.languages.join(', '); + } ngOnInit(): void { + this.corpusService.currentCorpus.subscribe(this.setCorpus.bind(this)); + } + + setCorpus(corpus: Corpus) { + this.corpus = corpus; + this.apiService.corpusdescription({filename: corpus.descriptionpage, corpus: corpus.name}).then( + doc => this.description = marked.parse(doc) + ); } } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index d7fa08b19..2d652cc1b 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -249,7 +249,7 @@ export class ApiService extends Resource { }) public corpusdescription: ResourceMethod< { filename: string; corpus: string }, - any + string >; $getUrl(actionOptions: IResourceAction): string | Promise { From 6433340f43f64de5f6cfb85ee762700130924b32 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 15:36:40 +0200 Subject: [PATCH 155/262] working tabs --- .../corpus-info/corpus-info.component.html | 30 +++++++++------ .../app/corpus-info/corpus-info.component.ts | 37 ++++++++++++++++--- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index 690d871a1..843118c0b 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -13,26 +13,34 @@

Period: {{minYear}}-{{maxYear}}

-
-
- {{corpus.title}} -
+
+ {{corpus.title}}

-
-
+
+
-
+ +
+ +
+ Fields! +
+ +
+
diff --git a/frontend/src/app/corpus-info/corpus-info.component.ts b/frontend/src/app/corpus-info/corpus-info.component.ts index bd11d8c81..2781bef33 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.ts +++ b/frontend/src/app/corpus-info/corpus-info.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; -import { ApiService, CorpusService } from '../services'; +import { ApiService, CorpusService, WordmodelsService } from '../services'; import { Corpus } from '../models'; import { marked } from 'marked'; +import { BehaviorSubject } from 'rxjs'; @Component({ selector: 'ia-corpus-info', @@ -12,8 +13,29 @@ export class CorpusInfoComponent implements OnInit { corpus: Corpus; description: string; + wordModelDocumentation: string; - constructor(private corpusService: CorpusService, private apiService: ApiService) { } + tabs = [ + { + name: 'general', + title: 'General information', + property: 'descriptionpage', + }, { + name: 'fields', + title: 'Fields', + property: 'fields', + }, { + name: 'models', + title: 'Word models', + property: 'word_models_present', + } + ]; + + currentTab = new BehaviorSubject<'general'|'fields'|'models'>( + 'general' + ); + + constructor(private corpusService: CorpusService, private apiService: ApiService, private wordModelsService: WordmodelsService) { } get minYear() { return this.corpus.minDate.getFullYear(); @@ -33,9 +55,14 @@ export class CorpusInfoComponent implements OnInit { setCorpus(corpus: Corpus) { this.corpus = corpus; - this.apiService.corpusdescription({filename: corpus.descriptionpage, corpus: corpus.name}).then( - doc => this.description = marked.parse(doc) - ); + this.apiService.corpusdescription({filename: corpus.descriptionpage, corpus: corpus.name}) + .then(marked.parse) + .then(doc => this.description = doc); + if (this.corpus.word_models_present) { + this.wordModelsService.wordModelsDocumentationRequest({corpus_name: this.corpus.name}) + .then(result => marked.parse(result.documentation)) + .then(doc => this.wordModelDocumentation = doc); + } } } From 2177983701be1b518091aed2740135223d1bbcdb Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 15:42:47 +0200 Subject: [PATCH 156/262] add field info component --- frontend/src/app/app.module.ts | 2 ++ .../corpus-info/corpus-info.component.html | 2 +- .../field-info/field-info.component.html | 1 + .../field-info/field-info.component.scss | 0 .../field-info/field-info.component.spec.ts | 23 +++++++++++++++++++ .../field-info/field-info.component.ts | 17 ++++++++++++++ 6 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/corpus-info/field-info/field-info.component.html create mode 100644 frontend/src/app/corpus-info/field-info/field-info.component.scss create mode 100644 frontend/src/app/corpus-info/field-info/field-info.component.spec.ts create mode 100644 frontend/src/app/corpus-info/field-info/field-info.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5ce3136f7..9867da12d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -91,6 +91,7 @@ import { DocumentPageComponent } from './document-page/document-page.component'; import { CorpusSelectorComponent } from './corpus-selection/corpus-selector/corpus-selector.component'; import { CorpusFilterComponent } from './corpus-selection/corpus-filter/corpus-filter.component'; import { CorpusInfoComponent } from './corpus-info/corpus-info.component'; +import { FieldInfoComponent } from './corpus-info/field-info/field-info.component'; export const appRoutes: Routes = [ @@ -192,6 +193,7 @@ export const declarations: any[] = [ DropdownComponent, ErrorComponent, FilterManagerComponent, + FieldInfoComponent, FooterComponent, FreqtableComponent, FullDataButtonComponent, diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index 843118c0b..fdbe266b7 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -37,7 +37,7 @@
- Fields! +
diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.html b/frontend/src/app/corpus-info/field-info/field-info.component.html new file mode 100644 index 000000000..32a4c9744 --- /dev/null +++ b/frontend/src/app/corpus-info/field-info/field-info.component.html @@ -0,0 +1 @@ +

{{field.displayName}}

diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.scss b/frontend/src/app/corpus-info/field-info/field-info.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.spec.ts b/frontend/src/app/corpus-info/field-info/field-info.component.spec.ts new file mode 100644 index 000000000..43558438f --- /dev/null +++ b/frontend/src/app/corpus-info/field-info/field-info.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { FieldInfoComponent } from './field-info.component'; +import { commonTestBed } from 'src/app/common-test-bed'; + +describe('FieldInfoComponent', () => { + let component: FieldInfoComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + commonTestBed().testingModule.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FieldInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.ts b/frontend/src/app/corpus-info/field-info/field-info.component.ts new file mode 100644 index 000000000..fc6de25b4 --- /dev/null +++ b/frontend/src/app/corpus-info/field-info/field-info.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { CorpusField } from '../../models'; + +@Component({ + selector: 'ia-field-info', + templateUrl: './field-info.component.html', + styleUrls: ['./field-info.component.scss'] +}) +export class FieldInfoComponent implements OnInit { + @Input() field: CorpusField; + + constructor() { } + + ngOnInit(): void { + } + +} From 1a8f95aadc84953ffab0022708d151b43f2f0502 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 15:48:59 +0200 Subject: [PATCH 157/262] add basic field info --- .../corpus-info/corpus-info.component.html | 4 +- .../field-info/field-info.component.html | 37 ++++++++++++++++++- .../field-info/field-info.component.scss | 9 +++++ .../field-info/field-info.component.ts | 9 +++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index fdbe266b7..e83c0e550 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -37,7 +37,9 @@
- +
+ +
diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.html b/frontend/src/app/corpus-info/field-info/field-info.component.html index 32a4c9744..9659396af 100644 --- a/frontend/src/app/corpus-info/field-info/field-info.component.html +++ b/frontend/src/app/corpus-info/field-info/field-info.component.html @@ -1 +1,36 @@ -

{{field.displayName}}

+
+ +
+

{{field.displayName}}

+

{{field.description}}

+
+
+ +
+ +
+

Type of data: {{mappingNames[field.mappingType]}}

+ +
    +
  • This field {{field.searchable ? 'can' : 'cannot'}} be searched
  • +
  • This field {{field.searchFilter ? 'has' : 'does not have'}} a search filter
  • +
+ + +

+ This field supports the following options for text analysis: +

+
    +
  • + Counting the total number of words, and calculating term frequencies relative to the total word count. +
  • +
  • + Removing stopwords +
  • +
  • + Stemming +
  • +
+
+
+
diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.scss b/frontend/src/app/corpus-info/field-info/field-info.component.scss index e69de29bb..77d11fcec 100644 --- a/frontend/src/app/corpus-info/field-info/field-info.component.scss +++ b/frontend/src/app/corpus-info/field-info/field-info.component.scss @@ -0,0 +1,9 @@ +.foldable-header { + display: inline-block; + vertical-align: text-top; + margin-left: 0.5rem; +} + +summary { + cursor: pointer; +} diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.ts b/frontend/src/app/corpus-info/field-info/field-info.component.ts index fc6de25b4..a35bfd262 100644 --- a/frontend/src/app/corpus-info/field-info/field-info.component.ts +++ b/frontend/src/app/corpus-info/field-info/field-info.component.ts @@ -9,6 +9,15 @@ import { CorpusField } from '../../models'; export class FieldInfoComponent implements OnInit { @Input() field: CorpusField; + mappingNames = { + text: 'text', + keyword: 'categorical', + integer: 'numeric', + float: 'numeric', + date: 'date', + boolean: 'binary' + }; + constructor() { } ngOnInit(): void { From 638db7d6b3778277207f920b60caa173ea7fb8c1 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 17:40:13 +0200 Subject: [PATCH 158/262] add backend view for field coverage --- backend/visualization/field_stats.py | 52 +++++++++++++++++++ .../visualization/tests/test_field_stats.py | 20 +++++++ backend/visualization/urls.py | 1 + backend/visualization/views.py | 14 +++++ 4 files changed, 87 insertions(+) create mode 100644 backend/visualization/field_stats.py create mode 100644 backend/visualization/tests/test_field_stats.py diff --git a/backend/visualization/field_stats.py b/backend/visualization/field_stats.py new file mode 100644 index 000000000..5ac27c680 --- /dev/null +++ b/backend/visualization/field_stats.py @@ -0,0 +1,52 @@ +from ianalyzer.elasticsearch import elasticsearch +from es.search import total_hits, search +from addcorpus.load_corpus import load_corpus +from visualization.query import MATCH_ALL + +def count_field(es_client, corpus_name, fieldname): + ''' + The absolute of documents that has a value for this field + ''' + + body = {'query': {'exists': {'field': fieldname}}} + result = search( + corpus=corpus_name, + query_model=body, + client=es_client, + size=0, + track_total_hits=True, + ) + + return total_hits(result) + + +def count_total(es_client, corpus_name): + ''' + The total number of documents in the corpus + ''' + + result = search( + corpus=corpus_name, + client=es_client, + query_model=MATCH_ALL, + size=0, + track_total_hits=True, + ) + return total_hits(result) + +def report_coverage(corpus_name): + ''' + Returns a dict with the ratio of documents that have a value for each field in the corpus + ''' + + es_client = elasticsearch(corpus_name) + corpus = load_corpus(corpus_name) + + total = count_total(es_client, corpus_name) + + return { + field.name: count_field(es_client, corpus_name, field.name) / total + for field in corpus.fields + } + + diff --git a/backend/visualization/tests/test_field_stats.py b/backend/visualization/tests/test_field_stats.py new file mode 100644 index 000000000..0390e8a33 --- /dev/null +++ b/backend/visualization/tests/test_field_stats.py @@ -0,0 +1,20 @@ +from visualization.field_stats import * + +def test_count(mock_corpus, test_es_client, select_small_mock_corpus, index_mock_corpus, mock_corpus_specs): + total_docs = mock_corpus_specs['total_docs'] + + for field in mock_corpus_specs['fields']: + count = count_field(test_es_client, mock_corpus, field) + assert count == total_docs + + assert count_total(test_es_client, mock_corpus) == total_docs + +def test_report(mock_corpus, test_es_client, select_small_mock_corpus, index_mock_corpus, mock_corpus_specs): + report = report_coverage(mock_corpus) + + assert report == { + 'date': 1.0, + 'title': 1.0, + 'content': 1.0, + 'genre': 1.0, + } diff --git a/backend/visualization/urls.py b/backend/visualization/urls.py index b9ea66a93..0c74d2523 100644 --- a/backend/visualization/urls.py +++ b/backend/visualization/urls.py @@ -7,4 +7,5 @@ path('ngram', NgramView.as_view()), path('date_term_frequency', DateTermFrequencyView.as_view()), path('aggregate_term_frequency', AggregateTermFrequencyView.as_view()), + path('coverage', FieldCoverageView.as_view()) ] diff --git a/backend/visualization/views.py b/backend/visualization/views.py index 0e5281f7c..f7a0e3094 100644 --- a/backend/visualization/views.py +++ b/backend/visualization/views.py @@ -8,6 +8,8 @@ from django.conf import settings from rest_framework.permissions import IsAuthenticated from addcorpus.permissions import CorpusAccessPermission +from visualization.field_stats import report_coverage +from addcorpus.permissions import corpus_name_from_request logger = logging.getLogger() @@ -128,3 +130,15 @@ def post(self, request, *args, **kwargs): except Exception as e: logger.error(e) raise APIException('Could not set up term frequency generation.') + +class FieldCoverageView(APIView): + ''' + Get the coverage of each field in a corpus + ''' + + permission_classes = [IsAuthenticated, CorpusAccessPermission] + + def get(self, request, *args, **kwargs): + corpus = corpus_name_from_request(request) + report = report_coverage(corpus) + return Response(report) From 40a177d7df68ad28be5dedbbb395cebeb500b051 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 18:02:05 +0200 Subject: [PATCH 159/262] show field coverage in frontend --- backend/visualization/urls.py | 2 +- .../src/app/corpus-info/corpus-info.component.html | 2 +- frontend/src/app/corpus-info/corpus-info.component.ts | 7 ++++++- .../corpus-info/field-info/field-info.component.html | 8 ++++++++ .../app/corpus-info/field-info/field-info.component.ts | 10 ++++++++++ frontend/src/app/models/visualization.ts | 4 ++++ frontend/src/app/services/api.service.ts | 7 +++++++ 7 files changed, 37 insertions(+), 3 deletions(-) diff --git a/backend/visualization/urls.py b/backend/visualization/urls.py index 0c74d2523..7c943cdaf 100644 --- a/backend/visualization/urls.py +++ b/backend/visualization/urls.py @@ -7,5 +7,5 @@ path('ngram', NgramView.as_view()), path('date_term_frequency', DateTermFrequencyView.as_view()), path('aggregate_term_frequency', AggregateTermFrequencyView.as_view()), - path('coverage', FieldCoverageView.as_view()) + path('coverage/', FieldCoverageView.as_view()) ] diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index e83c0e550..e8fab23bb 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -38,7 +38,7 @@
- +
diff --git a/frontend/src/app/corpus-info/corpus-info.component.ts b/frontend/src/app/corpus-info/corpus-info.component.ts index 2781bef33..cbc611c2d 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.ts +++ b/frontend/src/app/corpus-info/corpus-info.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ApiService, CorpusService, WordmodelsService } from '../services'; -import { Corpus } from '../models'; +import { Corpus, FieldCoverage } from '../models'; import { marked } from 'marked'; import { BehaviorSubject } from 'rxjs'; @@ -14,6 +14,7 @@ export class CorpusInfoComponent implements OnInit { description: string; wordModelDocumentation: string; + fieldCoverage: FieldCoverage; tabs = [ { @@ -58,6 +59,9 @@ export class CorpusInfoComponent implements OnInit { this.apiService.corpusdescription({filename: corpus.descriptionpage, corpus: corpus.name}) .then(marked.parse) .then(doc => this.description = doc); + this.apiService.fieldCoverage(corpus.name).then( + result => this.fieldCoverage = result + ); if (this.corpus.word_models_present) { this.wordModelsService.wordModelsDocumentationRequest({corpus_name: this.corpus.name}) .then(result => marked.parse(result.documentation)) @@ -65,4 +69,5 @@ export class CorpusInfoComponent implements OnInit { } } + } diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.html b/frontend/src/app/corpus-info/field-info/field-info.component.html index 9659396af..d096ac884 100644 --- a/frontend/src/app/corpus-info/field-info/field-info.component.html +++ b/frontend/src/app/corpus-info/field-info/field-info.component.html @@ -32,5 +32,13 @@

{{field.displayName}}

+ +

+ {{coveragePercentage}}% of the documents in this corpus have a value for this field +

+ +

+ Loading coverage data... +

diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.ts b/frontend/src/app/corpus-info/field-info/field-info.component.ts index a35bfd262..d7add40e7 100644 --- a/frontend/src/app/corpus-info/field-info/field-info.component.ts +++ b/frontend/src/app/corpus-info/field-info/field-info.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { CorpusField } from '../../models'; +import * as _ from 'lodash'; @Component({ selector: 'ia-field-info', @@ -8,6 +9,7 @@ import { CorpusField } from '../../models'; }) export class FieldInfoComponent implements OnInit { @Input() field: CorpusField; + @Input() coverage: number; mappingNames = { text: 'text', @@ -20,6 +22,14 @@ export class FieldInfoComponent implements OnInit { constructor() { } + get coveragePercentage() { + if (this.coverage) { + return (this.coverage * 100).toPrecision(3); + } else { + return this.coverage; // return undefined or 0 as-is + } + } + ngOnInit(): void { } diff --git a/frontend/src/app/models/visualization.ts b/frontend/src/app/models/visualization.ts index f353a97f9..753d481dd 100644 --- a/frontend/src/app/models/visualization.ts +++ b/frontend/src/app/models/visualization.ts @@ -124,3 +124,7 @@ export const barChartSetNull: Object = { normalize: null, visualizeTerm: null }; + +export interface FieldCoverage { + [field: string]: number; +}; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 2d652cc1b..bebf8d4af 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -24,6 +24,7 @@ import { DateTermFrequencyParameters, Download, DownloadOptions, + FieldCoverage, FoundDocument, LimitedResultsDownloadParameters, QueryDb, @@ -252,6 +253,12 @@ export class ApiService extends Resource { string >; + fieldCoverage(corpusName: string): Promise { + return this.http.get( + `/api/visualization/coverage/${corpusName}`, + ).toPromise(); + } + $getUrl(actionOptions: IResourceAction): string | Promise { const urlPromise = super.$getUrl(actionOptions); if (!this.apiUrl) { From cb57caa50cc1dda7bf413b85b324104abfeba383 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 18:17:27 +0200 Subject: [PATCH 160/262] references to info page --- .../corpus-header/corpus-header.component.ts | 24 +++---------------- .../corpus-selector.component.html | 3 ++- frontend/src/app/search/search.component.ts | 4 ---- .../word-models/word-models.component.html | 2 +- .../app/word-models/word-models.component.ts | 10 +------- 5 files changed, 7 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/corpus-header/corpus-header.component.ts b/frontend/src/app/corpus-header/corpus-header.component.ts index 5b7e938dd..1cc8fe988 100644 --- a/frontend/src/app/corpus-header/corpus-header.component.ts +++ b/frontend/src/app/corpus-header/corpus-header.component.ts @@ -1,8 +1,6 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { faDiagramProject, faInfo, faInfoCircle, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { faDiagramProject, faInfo, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { Corpus } from '../models'; -import { DialogService } from '../services'; @Component({ selector: 'ia-corpus-header', @@ -11,8 +9,7 @@ import { DialogService } from '../services'; }) export class CorpusHeaderComponent implements OnChanges, OnInit { @Input() corpus: Corpus; - @Input() currentPage: 'search'|'word-models'|'document'; - @Input() modelDocumentation: string; + @Input() currentPage: 'search'|'word-models'|'document'|'info'; searchIcon = faMagnifyingGlass; wordModelsIcon = faDiagramProject; @@ -20,9 +17,7 @@ export class CorpusHeaderComponent implements OnChanges, OnInit { wordModelsPresent: boolean; - faInfo = faInfoCircle; - - constructor(private dialogService: DialogService) { + constructor() { } ngOnInit() { @@ -34,17 +29,4 @@ export class CorpusHeaderComponent implements OnChanges, OnInit { this.wordModelsPresent = this.corpus.word_models_present; } } - - public showCorpusInfo(corpus: Corpus) { - this.dialogService.showDescriptionPage(corpus); - } - - public showModelInfo() { - this.dialogService.showDocumentation( - this.corpus.name + '_wm', - `Word models of ${this.corpus.title}`, - this.modelDocumentation, - ); - } - } diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html index 2c84b5973..f57bea8da 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -11,7 +11,8 @@

diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index b220b4479..517ea5f3d 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -115,10 +115,6 @@ export class SearchComponent extends ParamDirective { this.dialogService.showManualPage('query'); } - public showCorpusInfo(corpus: Corpus) { - this.dialogService.showDescriptionPage(corpus); - } - public switchTabs(index: number) { this.tabIndex = index; } diff --git a/frontend/src/app/word-models/word-models.component.html b/frontend/src/app/word-models/word-models.component.html index 75582b796..99cf47453 100644 --- a/frontend/src/app/word-models/word-models.component.html +++ b/frontend/src/app/word-models/word-models.component.html @@ -1,4 +1,4 @@ - +
diff --git a/frontend/src/app/word-models/word-models.component.ts b/frontend/src/app/word-models/word-models.component.ts index 9deba1b3f..57c10ff01 100644 --- a/frontend/src/app/word-models/word-models.component.ts +++ b/frontend/src/app/word-models/word-models.component.ts @@ -18,7 +18,7 @@ export class WordModelsComponent implements DoCheck, OnInit { user: User; corpus: Corpus; - modelDocumentation: any; + queryText: string; asTable = false; @@ -78,17 +78,9 @@ export class WordModelsComponent implements DoCheck, OnInit { if (!this.corpus.word_models_present) { this.router.navigate(['search', this.corpus.name]); } - this.getDocumentation(); } } - getDocumentation() { - this.wordModelsService - .wordModelsDocumentationRequest({ corpus_name: this.corpus.name }) - .then((result) => { - this.modelDocumentation = result.documentation; - }); - } submitQuery(): void { this.errorMessage = undefined; From 3ab6c6ded4034780649467f793366b45c730d0c9 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 18:41:25 +0200 Subject: [PATCH 161/262] fix errors for corpora without descriptions --- frontend/src/app/corpus-info/corpus-info.component.html | 2 +- frontend/src/app/corpus-info/corpus-info.component.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index e8fab23bb..0328236e3 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -38,7 +38,7 @@
- +
diff --git a/frontend/src/app/corpus-info/corpus-info.component.ts b/frontend/src/app/corpus-info/corpus-info.component.ts index cbc611c2d..3d6ec6098 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.ts +++ b/frontend/src/app/corpus-info/corpus-info.component.ts @@ -56,9 +56,13 @@ export class CorpusInfoComponent implements OnInit { setCorpus(corpus: Corpus) { this.corpus = corpus; - this.apiService.corpusdescription({filename: corpus.descriptionpage, corpus: corpus.name}) + if (corpus.descriptionpage) { + this.apiService.corpusdescription({filename: corpus.descriptionpage, corpus: corpus.name}) .then(marked.parse) .then(doc => this.description = doc); + } else { + this.currentTab.next('fields'); + } this.apiService.fieldCoverage(corpus.name).then( result => this.fieldCoverage = result ); From 332ecfc13a9721b8e023e3290521e1d87cb50e4f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 10 May 2023 18:41:34 +0200 Subject: [PATCH 162/262] html polishing --- .../src/app/corpus-info/corpus-info.component.html | 8 ++++---- .../src/app/corpus-info/corpus-info.component.scss | 13 +++++++++++++ .../field-info/field-info.component.html | 4 ++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index 0328236e3..5772f4ca2 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -7,10 +7,10 @@

{{corpus.description}}

-
-

Language: {{languages}}

-

Type: {{corpus.category}}

-

Period: {{minYear}}-{{maxYear}}

+
diff --git a/frontend/src/app/corpus-info/corpus-info.component.scss b/frontend/src/app/corpus-info/corpus-info.component.scss index e69de29bb..72d1d95c3 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.scss +++ b/frontend/src/app/corpus-info/corpus-info.component.scss @@ -0,0 +1,13 @@ +.heading { + font-size: 14px; +} + +.metadata { + letter-spacing: 1px; + text-transform: uppercase; + font-size: small; + + p { + margin-bottom: 0.25rem; + } +} diff --git a/frontend/src/app/corpus-info/field-info/field-info.component.html b/frontend/src/app/corpus-info/field-info/field-info.component.html index d096ac884..c50735816 100644 --- a/frontend/src/app/corpus-info/field-info/field-info.component.html +++ b/frontend/src/app/corpus-info/field-info/field-info.component.html @@ -1,9 +1,9 @@
-
+

{{field.displayName}}

{{field.description}}

-
+

From c72b39d59c175610c244d7d93926fa0c0052ba75 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 11 May 2023 10:16:28 +0200 Subject: [PATCH 163/262] add common properties to corpus class --- .../src/app/corpus-info/corpus-info.component.html | 4 ++-- .../src/app/corpus-info/corpus-info.component.ts | 12 ------------ .../corpus-selector/corpus-selector.component.html | 4 ++-- .../corpus-selector/corpus-selector.component.ts | 12 ------------ frontend/src/app/models/corpus.ts | 11 +++++++++++ 5 files changed, 15 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/corpus-info/corpus-info.component.html b/frontend/src/app/corpus-info/corpus-info.component.html index 5772f4ca2..d97f0a875 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.html +++ b/frontend/src/app/corpus-info/corpus-info.component.html @@ -8,9 +8,9 @@

{{corpus.description}}

diff --git a/frontend/src/app/corpus-info/corpus-info.component.ts b/frontend/src/app/corpus-info/corpus-info.component.ts index 3d6ec6098..a1fe9b065 100644 --- a/frontend/src/app/corpus-info/corpus-info.component.ts +++ b/frontend/src/app/corpus-info/corpus-info.component.ts @@ -38,18 +38,6 @@ export class CorpusInfoComponent implements OnInit { constructor(private corpusService: CorpusService, private apiService: ApiService, private wordModelsService: WordmodelsService) { } - get minYear() { - return this.corpus.minDate.getFullYear(); - } - - get maxYear() { - return this.corpus.maxDate.getFullYear(); - } - - get languages() { - return this.corpus.languages.join(', '); - } - ngOnInit(): void { this.corpusService.currentCorpus.subscribe(this.setCorpus.bind(this)); } diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html index f57bea8da..88b1e483c 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -28,9 +28,9 @@

-

Language: {{languages}}

+

Language: {{corpus.displayLanguages}}

Type: {{corpus.category}}

-

Period: {{minYear}}-{{maxYear}}

+

Period: {{corpus.minYear}}-{{corpus. maxYear}}

diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts index 8aac8e160..95d4ac5a8 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.ts @@ -18,18 +18,6 @@ export class CorpusSelectorComponent implements OnInit { constructor(private dialogService: DialogService, private router: Router) { } - get minYear() { - return this.corpus.minDate.getFullYear(); - } - - get maxYear() { - return this.corpus.maxDate.getFullYear(); - } - - get languages() { - return this.corpus.languages.join(', '); - } - ngOnInit(): void { } diff --git a/frontend/src/app/models/corpus.ts b/frontend/src/app/models/corpus.ts index ea779501b..d947e9639 100644 --- a/frontend/src/app/models/corpus.ts +++ b/frontend/src/app/models/corpus.ts @@ -30,6 +30,17 @@ export class Corpus implements ElasticSearchIndex { public documentContext?: DocumentContext, ) { } + get minYear(): number { + return this.minDate.getFullYear(); + } + + get maxYear(): number { + return this.maxDate.getFullYear(); + } + + get displayLanguages(): string { + return this.languages.join(', '); // may have to truncate long lists? + } } export interface ElasticSearchIndex { From a32f78270a2b1cb14e99ae3bf2069d9393ab4af1 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 24 May 2023 16:21:15 +0200 Subject: [PATCH 164/262] always show info link in corpus selector --- .../corpus-selector/corpus-selector.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html index 88b1e483c..4f2305a67 100644 --- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html +++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.html @@ -10,7 +10,7 @@

-

diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts index c9fb4068a..06dd788e3 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -92,6 +92,22 @@ export class CorpusFilterComponent implements OnInit { }; } + setYear(subject: BehaviorSubject, value: string|Date) { + let valueAsDate: Date; + if (typeof(value) == 'string') { + const parsed = Date.parse(value); + if (!_.isNaN(parsed)) { + valueAsDate = new Date(parsed); + } + } else { + valueAsDate = value; + } + + if (valueAsDate) { + subject.next(valueAsDate); + } + } + reset() { this.selection.forEach(subject => subject.next(undefined)); } From 656cc3da35d905e393001bfe5b3f4fc856294c28 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 5 Jun 2023 18:19:40 +0200 Subject: [PATCH 186/262] outfactor date picker to own component --- frontend/src/app/app.module.ts | 2 + .../corpus-filter.component.html | 16 +++----- .../corpus-filter/corpus-filter.component.ts | 16 -------- .../date-picker/date-picker.component.html | 7 ++++ .../date-picker/date-picker.component.scss | 0 .../date-picker/date-picker.component.spec.ts | 24 ++++++++++++ .../date-picker/date-picker.component.ts | 39 +++++++++++++++++++ 7 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html create mode 100644 frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.scss create mode 100644 frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.spec.ts create mode 100644 frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5d8b7ef8f..8d282a102 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -90,6 +90,7 @@ import { VerifyEmailComponent } from './login/verify-email/verify-email.componen import { DocumentPageComponent } from './document-page/document-page.component'; import { CorpusSelectorComponent } from './corpus-selection/corpus-selector/corpus-selector.component'; import { CorpusFilterComponent } from './corpus-selection/corpus-filter/corpus-filter.component'; +import { DatePickerComponent } from './corpus-selection/corpus-filter/date-picker/date-picker.component'; export const appRoutes: Routes = [ @@ -175,6 +176,7 @@ export const declarations: any[] = [ CorpusHeaderComponent, CorpusSelectionComponent, CorpusSelectorComponent, + DatePickerComponent, DateFilterComponent, DialogComponent, DocumentPageComponent, diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html index 4515dd545..c5f973119 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.html @@ -21,21 +21,17 @@
- +
-
- +
diff --git a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts index 06dd788e3..c9fb4068a 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/corpus-filter.component.ts @@ -92,22 +92,6 @@ export class CorpusFilterComponent implements OnInit { }; } - setYear(subject: BehaviorSubject, value: string|Date) { - let valueAsDate: Date; - if (typeof(value) == 'string') { - const parsed = Date.parse(value); - if (!_.isNaN(parsed)) { - valueAsDate = new Date(parsed); - } - } else { - valueAsDate = value; - } - - if (valueAsDate) { - subject.next(valueAsDate); - } - } - reset() { this.selection.forEach(subject => subject.next(undefined)); } diff --git a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html new file mode 100644 index 000000000..0b75ded5e --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html @@ -0,0 +1,7 @@ + + diff --git a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.scss b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.spec.ts b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.spec.ts new file mode 100644 index 000000000..6e8a47d30 --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { DatePickerComponent } from './date-picker.component'; +import { commonTestBed } from '../../../common-test-bed'; + +describe('DatePickerComponent', () => { + let component: DatePickerComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + commonTestBed().testingModule.compileComponents(); + })); + + + beforeEach(() => { + fixture = TestBed.createComponent(DatePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts new file mode 100644 index 000000000..fe70b66e1 --- /dev/null +++ b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts @@ -0,0 +1,39 @@ +import { Component, Input, Output } from '@angular/core'; +import * as _ from 'lodash'; +import * as moment from 'moment'; +import { BehaviorSubject } from 'rxjs'; + +@Component({ + selector: 'ia-date-picker', + templateUrl: './date-picker.component.html', + styleUrls: ['./date-picker.component.scss'] +}) +export class DatePickerComponent { + @Input() @Output() subject: BehaviorSubject = new BehaviorSubject(undefined); + @Input() minDate: Date; + @Input() maxDate: Date; + @Input() default: Date; + @Input() unit: 'year'|'date' = 'year'; + + constructor() { } + + get dateFormat(): string { + return this.unit === 'year' ? 'yy' : 'dd-mm-yy'; + } + + set(value: string|Date) { + let valueAsDate: Date; + if (typeof(value) == 'string') { + const format = this.unit === 'year' ? 'YYYY' : 'DD-MM-YYYY'; + const m = moment(value, format); + if (m.isValid()) { + valueAsDate = m.toDate(); + } + } else { + valueAsDate = value; + } + + this.subject.next(valueAsDate); + } + +} From d3673b43f25fb73b03ed3c7faad6751e54176ecf Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 6 Jun 2023 16:50:00 +0200 Subject: [PATCH 187/262] remove clutter from readme --- README.md | 59 +++-------------------------- documentation/Email.md | 13 +++++++ documentation/Indexing-corpora.md | 37 ++++++++++++++++++ documentation/Indexing-on-server.md | 9 +++++ documentation/Overview.md | 22 +++++++++++ 5 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 documentation/Email.md create mode 100644 documentation/Indexing-corpora.md diff --git a/README.md b/README.md index 14e7b61c9..665d5ba64 100644 --- a/README.md +++ b/README.md @@ -6,29 +6,7 @@ The text mining tool that obviates all others. I-analyzer is a web application that allows users to search through large text corpora, requiring no experience in text mining or technical know-how. -## Directory structure - -The I-analyzer backend (`/backend`) is a python/Django app that provides the following functionality: - -- A 'users' module that defines user accounts. - -- A 'corpora' module containing corpus definitions and metadata of the currently implemented corpora. For each corpus added in I-analyzer, this module defines how to extract document contents from its source files and sets parameters for displaying the corpus in the interface, such as sorting options. - -- An 'addcorpus' module which manages the functionality to extract data from corpus source files (given the definition) and save this in an elasticsearch index. Source files can be XML or HTML format (which are parsed with `beautifulsoup4` + `lxml`) or CSV. This module also provides the basic data structure for corpora. - -- An 'es' module which handles the communication with elasticsearch. The data is passed through to the index using the `elasticsearch` package for Python (note that `elasticsearch-dsl` is not used, since its [documentation](https://elasticsearch-dsl.readthedocs.io/en/latest) at the time seemed less immediately accessible than the [low-level](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) version). - -- An 'api' module that that enables users to search through an ElasticSearch index of a text corpus and stream search results into a CSV file. The module also performs more complex analysis of search results for visualisations. - -- A 'visualizations' module that does the analysis for several types of text-based visualisations. - -- A 'downloads' module that collects results into csv files. - -- A 'wordmodels' module that handles functionality related to word embeddings. - -`ianalyzer/frontend` is an [Angular 13](https://angular.io/) web interface. - -See the documentation for [a more extensive overview](./documentation/Overview.md) +See the documentation for [an overview of the repository](./documentation/Overview.md) ## Prerequisites @@ -38,8 +16,6 @@ See the documentation for [a more extensive overview](./documentation/Overview.m * [Redis](https://www.redis.io/) (used by [Celery](http://www.celeryproject.org/)). Recommended installation is [installing from source](https://redis.io/docs/getting-started/installation/install-redis-from-source/) * Yarn -If you wish to have email functionality, also make sure you have an email server set up, such as [maildev](https://maildev.github.io/maildev/). - The documentation includes a [recipe for installing the prerequisites on Debian 10](./documentation/Local-Debian-I-Analyzer-setup.md) ## First-time setup @@ -77,40 +53,16 @@ yarn postinstall The backend readme provides more details on these steps. 8. Set up the database and migrations by running `yarn django migrate`. 9. Make a superuser account with `yarn django createsuperuser` -10. In `frontend/src/environments`, create a file `environment.private.ts` with the following settings: -``` -privateEnvironment = { - appName: I-Analyzer, - aboutPage: ianalyzer -} -``` ## Adding corpora To include corpora on your environment, you need to index them from their source files. The source files are not included in this directory; ask another developer about their availability. If you have (a sample of) the source files for a corpus, you can add it your our environment as follows: -_Note:_ these instructions are for adding a corpus that already has a corpus definition. For adding new corpus definitions, see [How to add a new corpus to I-analyzer](./documentation/How-to-add-a-new-corpus-to-Ianalyzer.md). +_Note:_ these instructions are for indexing a corpus that already has a corpus definition. For adding new corpus definitions, see [How to add a new corpus to I-analyzer](./documentation/How-to-add-a-new-corpus-to-Ianalyzer.md). 1. Add the corpus to the `CORPORA` dictionary in your local settings file. The key should match the class name of the corpus definition. This match is not case-sensitive, and your key may include extra non-alphabetic characters (they will be ignored when matching). The value should be the absolute path the corpus definition file (e.g. `.../backend/corpora/times/times.py`). -2. Set configurations for your corpus. Check the definition file to see which variables it expects to find in the configuration. Some of these may already be set in settings.py, but you will at least need to define the name of the elasticsearch index and the (absolute) path to your source files. -3. Activate your python virtual environment. Create an ElasticSearch index from the source files by running, e.g., `yarn django index dutchannualreports -s 1785-01-01 -e 2010-12-31`, for indexing the Dutch Annual Reports corpus starting in 1785 and ending in 2010. The dates are optional, and default to specified minimum and maximum dates of the corpus. (Note that new indices are created with `number_of_replicas` set to 0 (this is to make index creation easier/lighter). In production, you can automatically update this setting after index creation by adding the `--prod` flag (e.g. `yarn django index goodreads --prod`). Note though, that the -`--prod` flag creates a _versioned_ index name, which needs an alias to actually work as `name_of_index_without_version` (see below for more details). - -#### Flags of indexing script -- --prod / -p Whether or not to create a versioned index name -- --mappings_only / -m Whether to only create an index with mappings and settings, without adding data to it (useful before reindexing from another index or another server) -- --add / -a Add documents to an existing index (skip index creation) -- --update / -u Add or change fields in the documents. This requires an `update_body` or `update_script` to be set in the corpus definition, see [example for update_body in dutchnewspapers](backend/corpora/dutchnewspapers/dutchnewspapers_all.py) and [example for update_script in goodreads](backend/corpora/goodreads/goodreads.py). -- --delete / -d Delete an existing index with the `corpus.es_index` name. Note that in production, `corpus.es_index` will usually be an *alias*, and you would use the `yarn django es alias -c corpus-name --clean` to achieve the same thing. -- --rollover / -r Only applies in production: rollover a versioned index to the newest version. This *will not* delete the old index (so you have a chance to check the new index and roll back, if necessary) - -#### Production - -On the servers, we work with aliases. Indices created with the `--prod` flag will have a version number (e.g. `indexname-1`), and as such will not be recognized by the corpus definition (which is looking for `indexname`). Create an alias for that using the `alias` command: `yarn django alias -c corpusname`. That script ensures that an alias is present for the index with the highest version numbers, and not for all others (i.e. older versions). The advantage of this approach is that an old version of the index can be kept in place as long as is needed, for example while a new version of the index is created. Note that removing an alias does not remove the index itself. - -Once you have an alias in place, you might want to remove any old versions of the index. The `alias` command can be used for this. If you call `yarn django alias -c corpusname --clean` any versions of the index that are not the newest version will be removed. Note that removing an index also removes any existing aliases for it. You might want to perform this as a separate operation (i.e. after completing step 14) so that the old index stays in place for a bit while you check that all is fine. - -See the documentation for more information about [indexing on the server](./documentation/Indexing-on-server.md). +2. Set configurations for your corpus. Check the definition file to see which variables it expects to find in the configuration. Some of these may already be set in settings.py, but you will at least need to define the (absolute) path to your source files. +3. Activate your python virtual environment. Create an ElasticSearch index from the source files by running, e.g., `yarn django index dutchannualreports`, for indexing the Dutch Annual Reports corpus in a development environment. See [Indexing](documentation/Indexing-corpora.md) for more information. ## Running a dev environment @@ -118,8 +70,7 @@ See the documentation for more information about [indexing on the server](./docu 2. Activate your python environment. Start the backend server with `yarn start-back`. This creates an instance of the Django server at `127.0.0.1:8000`. 3. (optional) If you want to use celery, start your local redis server by running `redis-server` in a separate terminal. 4. (optional) If you want to use celery, activate your python environment. Run `yarn celery worker`. Celery is used for long downloads and the word cloud and ngrams visualisations. -5. (optional) If you want to use email functionality, start your local email server. -6. Start the frontend by running `yarn start-front`. +5. Start the frontend by running `yarn start-front`. ## Notes for development diff --git a/documentation/Email.md b/documentation/Email.md new file mode 100644 index 000000000..3abf52af4 --- /dev/null +++ b/documentation/Email.md @@ -0,0 +1,13 @@ +## Email + +The backend sends emails about account administration (veryfing emails, resetting passwords), and downloads. + +By default, the backend will use the django console backend for emails, so any outgoing mail will be displayed on your console. + +If you want to use a server like [maildev](https://maildev.github.io/maildev/), you can configure a different email backend in your local settings, for example: + +```python +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = '0.0.0.0' +EMAIL_PORT = '1025' +``` diff --git a/documentation/Indexing-corpora.md b/documentation/Indexing-corpora.md new file mode 100644 index 000000000..fdbadbf22 --- /dev/null +++ b/documentation/Indexing-corpora.md @@ -0,0 +1,37 @@ +# Indexing corpora + +Indexing is the step to load corpus data into elasticsearch, which makes the data available through the I-analyzer interface. + +You can start indexing once you have +- A python source file for the corpus +- A directory with source data +- Added the necessary properties to django settings + +The basic indexing command is: + +```bash +yarn django index my-corpus +``` + +Use `yarn django index --help` to see all possible flags. Some useful options are highlighted below. + +## Development + +For development environments, we usually maintain a single index per corpus, rather than creating versioned indices. New indices are also created with `number_of_replicas` set to 0 (this is to make index creation easier/lighter). + +Some options that may be useful for development: + +### Delete index before starting + +`--delete` / `-d` deletes an existing index of this name, if there is one. Without this flag, you will add your data to the existing index. + +### Date selection + +`--start` / `-s` and `--end` / `-e` respectively give a start and end date to select source files. Note that this only works if the `sources` function in your corpus definition makes use of these options; not all corpora have this defined. (It is not always possible to infer exact dates from source file metadata.) + +The filtering of source files may not be exact (e.g. only take the year into account). These flags do *not* filter documents based on their contents. + + +## Production + +See [Indexing on server](documentation/Indexing-on-server.md) for more information about production-specific settings. diff --git a/documentation/Indexing-on-server.md b/documentation/Indexing-on-server.md index 95fe96137..4493bfc68 100644 --- a/documentation/Indexing-on-server.md +++ b/documentation/Indexing-on-server.md @@ -1,5 +1,8 @@ # Indexing corpora on the server +For production environments, we use *versioned* index names (e.g. `times-1`, `times-2`), and use an alias (e.g. `times`) to point to the correct version. The advantage of this approach is that an old version of the index can be kept in place as long as is needed, for example while a new version of the index is created. + + ## Moving data to server On the server, move data to a location in the `/its` share. @@ -14,14 +17,20 @@ Start a screen with a descriptive name (e.g., `screen -S index-superb-corpus`). Call the flask command for indexing, e.g., `yarn django index superb-corpus -p`. The production flag indicates that we have a *versioned* index after this: `superb-corpus-1`. You can also choose to add the `--rollover` (`-r`) flag: this is equivalent with automaticaly calling `yarn django alias` after `yarn django index`. As it's advisable to double-check a new index before setting / rolling over the alias, this flag should be used with caution. ## Additional indexing flags + It is also possible to only add settings and mappings by providing the `--mappings-only` or `-m` flag. This is useful, for instance, before a `REINDEX` via Kibana from one Elasticsearch cluster to another (which is often faster than reindexing from source). +`--update` / `-u` can be used to run an update script for the corpus. This requires an `update_body` or `update_script` to be set in the corpus definition, see [example for update_body in dutchnewspapers](backend/corpora/dutchnewspapers/dutchnewspapers_all.py) and [example for update_script in goodreads](backend/corpora/goodreads/goodreads.py). + + ## Alias Either: - create an alias `superb-corpus` on Kibana manually: `PUT suberb-corpus-1/_alias/superb-corpus`. After this, the corpus will be reachable under the alias. - or: run `yarn django alias superb-corpus-name`. This will set an alias with the name defined by `es_alias` or (fallback) `es_index`. If you additionally provide the `--clean` flag, this will also remove the index with the lower version number. Naturally, this should only be used if the new index version has the expected number of documents, fields, etc., and the old index version is fully dispensable. +Note that removing an alias does not remove the index itself, but removing an index also removes any existing aliases for it. + ## Indexing from multiple corpus definitions If you have separate datasets for different parts of a corpus, you may combine them by setting the `ES_INDEX` variable in the corpus definitions to the same `overarching-corpus` index name. diff --git a/documentation/Overview.md b/documentation/Overview.md index b4964d10a..f8118bd2b 100644 --- a/documentation/Overview.md +++ b/documentation/Overview.md @@ -2,6 +2,28 @@ The application consists of a backend, implemented in [Django](https://www.djangoproject.com/) and a frontend implemented in [Angular](https://angular.io/). +## Directory structure + +The I-analyzer backend (`/backend`) is a python/Django app that provides the following functionality: + +- A 'users' module that defines user accounts. + +- A 'corpora' module containing corpus definitions and metadata of the currently implemented corpora. For each corpus added in I-analyzer, this module defines how to extract document contents from its source files and sets parameters for displaying the corpus in the interface, such as sorting options. + +- An 'addcorpus' module which manages the functionality to extract data from corpus source files (given the definition) and save this in an elasticsearch index. Source files can be XML or HTML format (which are parsed with `beautifulsoup4` + `lxml`) or CSV. This module also provides the basic data structure for corpora. + +- An 'es' module which handles the communication with elasticsearch. The data is passed through to the index using the `elasticsearch` package for Python (note that `elasticsearch-dsl` is not used, since its [documentation](https://elasticsearch-dsl.readthedocs.io/en/latest) at the time seemed less immediately accessible than the [low-level](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) version). + +- An 'api' module that that enables users to search through an ElasticSearch index of a text corpus and stream search results into a CSV file. The module also performs more complex analysis of search results for visualisations. + +- A 'visualizations' module that does the analysis for several types of text-based visualisations. + +- A 'downloads' module that collects results into csv files. + +- A 'wordmodels' module that handles functionality related to word embeddings. + +`ianalyzer/frontend` is an [Angular 13](https://angular.io/) web interface. + # Backend The backend has three responsibilities: From ee797054a6eabb28f4d787569ebdc78311709ce0 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 7 Jun 2023 10:29:20 +0200 Subject: [PATCH 188/262] fix tests --- backend/wordmodels/similarity.py | 20 ++----- .../tests/mock-word-models/model_1810_1839.wv | Bin 0 -> 20848 bytes .../model_1810_1839_vocab.pkl | Bin 2264 -> 0 bytes .../mock-word-models/model_1810_1899_full.w2v | Bin 17003 -> 0 bytes .../model_1810_1899_full_analyzer.pkl | Bin 445885 -> 0 bytes .../model_1810_1899_full_vocab.pkl | Bin 2204 -> 0 bytes .../tests/mock-word-models/model_1840_1869.wv | Bin 0 -> 20779 bytes .../model_1840_1869_vocab.pkl | Bin 2195 -> 0 bytes .../tests/mock-word-models/model_1870_1899.wv | Bin 0 -> 20799 bytes .../model_1870_1899_vocab.pkl | Bin 2215 -> 0 bytes .../wordmodels/tests/test_related_words.py | 12 ++--- backend/wordmodels/tests/test_similarity.py | 49 ++++++------------ backend/wordmodels/tests/test_wm_import.py | 21 ++------ backend/wordmodels/utils.py | 2 +- backend/wordmodels/views.py | 7 ++- backend/wordmodels/visualisations.py | 18 ++++--- frontend/src/app/models/search-results.ts | 1 - .../related-words/related-words.component.ts | 2 - 18 files changed, 45 insertions(+), 87 deletions(-) create mode 100644 backend/wordmodels/tests/mock-word-models/model_1810_1839.wv delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1810_1839_vocab.pkl delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1810_1899_full.w2v delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1810_1899_full_analyzer.pkl delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1810_1899_full_vocab.pkl create mode 100644 backend/wordmodels/tests/mock-word-models/model_1840_1869.wv delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1840_1869_vocab.pkl create mode 100644 backend/wordmodels/tests/mock-word-models/model_1870_1899.wv delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1870_1899_vocab.pkl diff --git a/backend/wordmodels/similarity.py b/backend/wordmodels/similarity.py index 2c24214c8..3a9d45602 100644 --- a/backend/wordmodels/similarity.py +++ b/backend/wordmodels/similarity.py @@ -1,7 +1,6 @@ import numpy as np -from wordmodels.utils import transform_query, index_to_term - +from wordmodels.utils import transform_query def term_similarity(wm, term1, term2): vectors = wm['vectors'] @@ -19,32 +18,23 @@ def find_n_most_similar(wm, query_term, n): """ transformed_query = transform_query(query_term) vectors = wm['vectors'] - vocab = vectors.index_to_key results = most_similar_items(vectors, transformed_query, n) return [{ 'key': result[0], 'similarity': result[1] } for result in results] -def most_similar_items(vectors, term, n, missing_terms = 0): +def most_similar_items(vectors, term, n): ''' Find the n most similar terms in a keyed vectors matrix, while filtering on the vocabulary. parameters: - `vectors`: the KeyedVectors - - `term`: the term for which to find the nearest neighbours. Should already have been - passed through the model's analyzer. + - `term`: the term for which to find the nearest neighbours, transformed with `transform_query` - `n`: number of neighbours to return - - `missing_terms`: used for recursion. indicates that of the `n` nearest vectors, `missing_terms` vectors - are not actually included in `vocab`, hence we should request `n + missing_terms` vectors ''' vocab = vectors.index_to_key if term in vocab: - results = vectors.most_similar(term, topn=n + missing_terms) - results_complete = len(results) == min(n, len(vocab) - 1) - if results_complete: - return results - else: - delta = n - len(results) - return most_similar_items(vectors, term, n, missing_terms=delta + missing_terms) + results = vectors.most_similar(term, topn=n) + return results return [] diff --git a/backend/wordmodels/tests/mock-word-models/model_1810_1839.wv b/backend/wordmodels/tests/mock-word-models/model_1810_1839.wv new file mode 100644 index 0000000000000000000000000000000000000000..076f7e549031d1ba80e6eb30fea824e7d3ae0442 GIT binary patch literal 20848 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�HJPLAnZInN<8n+L9BcyfWdJ!yuu}td$lqI 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 BO=$oC literal 0 HcmV?d00001 diff --git a/backend/wordmodels/tests/mock-word-models/model_1810_1839_vocab.pkl b/backend/wordmodels/tests/mock-word-models/model_1810_1839_vocab.pkl deleted file mode 100644 index d2b3fd88d39adda6f08b0369058bee142c6d8883..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2264 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 CSYt^5 diff --git a/backend/wordmodels/tests/mock-word-models/model_1810_1899_full_analyzer.pkl b/backend/wordmodels/tests/mock-word-models/model_1810_1899_full_analyzer.pkl deleted file mode 100644 index a5fcea0445c6be488e4020bc9ff55a81dbda8ad1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445885 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 G3bdbNcyJ0iJTW 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

Y4$ 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�j-o((f*kh396lN0l=& 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�e=lqqm=s-Nx1kvLN_ho+iG9~vBq_lRYK(0|Q& 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 diff --git a/backend/wordmodels/tests/mock-word-models/model_1810_1899_full_vocab.pkl b/backend/wordmodels/tests/mock-word-models/model_1810_1899_full_vocab.pkl deleted file mode 100644 index eaed24b2087220bd9ff9e9e37adfa0fa01fe9e66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2204 zcmX|?<-^=I494H}Ev#c^#*R8>W@frUxA`v0#l6J7Cw7{pgLcf!%*@P;f7TSqw{O3D z{7AB8Ik$Fx1?H9w7R}AgnW1kY)=|zQ)mWfBHr8Q&u(0fq7LgT)r1Yy7OsR??5?Z6 zb=YIDlQE8HNzTvOA%J<4$D;TC}1B+opX_W<=DB21z+KQDpQO7 zB`&u$U(W%m`Wnks^$lOlfs!q+eT9Pr$95IrVA<)|;}F%n&v2+jjcej=9wxa8li_eN z6sF8^L_*GbWLgh6Dv6x*Xh~WSj!}&`{8&j^aGx?9r$KyxV2+MB&Sd`*W&h_e~(2rWW7gbPe@w-%p$&yRtdXRB$*;QdpM}U+>wn z73-qMIcf+vS0Um&$!TN2`C?nyaGx&_oH(v@pD)C&ckgimATtr-_ zF*XETuh>7R8xr!qjmFm)YJ43q($!{EAWO(bQe%f)PEvJpF=MIaq|=46)V1Pv9xElS z^AQud7N(+q$7Rx6RATixoJc;;;Z-UyRf(S@RC1_gOWyH!D(I>s#3Ys-8a38fCEMY0 zJOr&en;JJJis&?^pOMun@^igOf~KY6^fwEmv5|0#a4l!MRRU|zPil?I^3v9d91MR5 zZcE75a=WAsro|nJI^3y526rVQ+$~)VL=_L^JsRJC2=A2&fgfOr`_#=Z?0z*Ac8Uiy zu#gYegQ{B|p@*ao!Q)}o`fuDL3BALknm1a``81r)C)O zv}}j_i)SPmldbV=GEjx*X6W$z%)!(5!i@RVzBsdVcqz&KbiDjOPw+~z*nn4)>^br^ zId|l9j|;pmv!y>G-bl8Z2ydp9T6}M5F3Y`mTl&M#{vADr_jp&mzU8k~zHS84^<2JNHqC!e=HHL3HT&Yhfl@V5M8iU{7ph0jBk^jIP|;3mT&O;8FQK+X3P`(qi(72_$jHL9X}_v!!Me;pNC%) x^54~O(vsHpt&R9yBW2Y7kb`xEKP5%SU%tPTe1pHm*zvm<;~&Y!T;pF_{vWgYKvMt! diff --git a/backend/wordmodels/tests/mock-word-models/model_1840_1869.wv b/backend/wordmodels/tests/mock-word-models/model_1840_1869.wv new file mode 100644 index 0000000000000000000000000000000000000000..8e6f2e4b69b0157cec4717807d3c30c7790c8346 GIT binary patch literal 20779 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
DUC-d~C^OU^c!YX=`$kr~ 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

SCpZFQXW0aB2q8FB*@ddh{Vgc@Jt^ghJ8gVPyBu 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 { return this.wordModelsService.getRelatedWords(this.queryText, this.corpus.name, this.neighbours) .then(results => { - this.totalSimilarities = results.total_similarities; this.totalData = results.similarities_over_time; this.timeIntervals = results.time_points; this.zoomedInData = results.similarities_over_time_local_top_n; From 4d49a942bddebe534c9f5d086b1792ace17c0b0e Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 7 Jun 2023 10:32:02 +0200 Subject: [PATCH 189/262] delete old word2vec representation of testing data --- .../tests/mock-word-models/model_1810_1839.w2v | Bin 17063 -> 0 bytes .../tests/mock-word-models/model_1840_1869.w2v | Bin 16994 -> 0 bytes .../tests/mock-word-models/model_1870_1899.w2v | Bin 17014 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1810_1839.w2v delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1840_1869.w2v delete mode 100644 backend/wordmodels/tests/mock-word-models/model_1870_1899.w2v diff --git a/backend/wordmodels/tests/mock-word-models/model_1810_1839.w2v b/backend/wordmodels/tests/mock-word-models/model_1810_1839.w2v deleted file mode 100644 index 5dcde190560cdc0330c06ce5c2b52d396cafc01d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17063 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 diff --git a/backend/wordmodels/tests/mock-word-models/model_1870_1899.w2v b/backend/wordmodels/tests/mock-word-models/model_1870_1899.w2v deleted file mode 100644 index 46b3f1406782d9614820953618cce47123b899d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17014 zcmXWicOX~a|2XiFnaohAUS^RZl5x-T6_t^Oii#o;qO6R(jY4E+G^|L8gb+>cdA=wu zX-88-lB8XlNuNUW^bPk3 zAEi+9-7$;QUcjk~!2w4ulmkzfv^NDb)$8Z*Rm4yk+2MA;Z^+ScK;?)j&l509dy- z&=ty(NG0^pGa!KX->+7Z@%S5TaaoHz%hg0S?FY3@{Y)mrj|CYjAiD1(@%TU}IxIR5 zqJ34s*ZW4^@J8u-*AhDMV=q||xPauF?4bS2+MDbTQuGYu-L6!}J(D-$)t8Di=Yb4g z{Z|mk7ajrG)boO-!MEg2%R0Vo{!Dl%J(qv+{VX!uFNfxNonzFVMiAYO3ux$cjus~F zrj};kLA9?IJ%e})RwRN`f-me~?!&J35)3bDg>!;MxVJ+cEef(wb>?Kcuv!unUW+pg zOuXfDn^pA4s%eb%TWdIZoQG*`V{k>odb;+rA#Qd&%aPu^a#LM&UadfMzlgF1Uq4ue z9Noz{ABr+px~fg9of64;yRVE$@IBJr(L;}&`bNi1+R1bq)X{)TW0}>i=gGG6EaLmo z39dZfiPdY9I5UdpoS02pcNH<&Ti;UmpY}Ak;yGMh!(ipSaCqAPlq5M=fb9Do@@#=B zD8+e$HVMTUKO#WIcqwekHpfj*7t*iCY7y`JuxyoyLC;Vh-rldOK=1V+)qRdBmKyv! zX;&de_biAU+(w`H)_{d+6#gVH;HsS$KO?Q)V)gHJf(*w@LGXmR0@Kai#Mp8GzXh2= zeT)RZy80G+hVht->X7#%7WCYlK;^dx;)o&Yd^=3LcJnQdMP4A`Y$e@vO%~cu$k2&X z#pukD)`T(zJ}+}Gn_X%@|7-(Z)f^4&Jt68B#!UD z4!Ij5;OK#+1p3w?yF7)Cw)U}g^Kw}@@QwLBTOXfqtYxm{PD3TT`Al)cb<2T3R*{_XD|$EPnseW3*AdGHd*yZMy8*rW%#JNIMz&mq$1 zH4&!wJtex0qL9;G!lox3B}81=a#N}!PClN2`@cG(m*>VUJjZu|@cnfqM&G)QV>=}U z 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 From 2e3bd17e149c63fffa4d578437fc57b67f95819a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 13:28:57 +0200 Subject: [PATCH 190/262] update documentation --- backend/corpora/dbnl/dbnl_metadata.py | 5 ++++- backend/corpora/dbnl/description/dbnl.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/corpora/dbnl/dbnl_metadata.py b/backend/corpora/dbnl/dbnl_metadata.py index 63fc76363..37af17db7 100644 --- a/backend/corpora/dbnl/dbnl_metadata.py +++ b/backend/corpora/dbnl/dbnl_metadata.py @@ -5,7 +5,10 @@ import corpora.dbnl.utils as utils class DBNLMetadata(CSVCorpus): - '''Helper corpus for extracting the DBNL metadata''' + '''Helper corpus for extracting the DBNL metadata. + + Used by the DBNL corpus for CSV extraction utilities - + not intended as a standalone corpus.''' data_directory = settings.DBNL_DATA diff --git a/backend/corpora/dbnl/description/dbnl.md b/backend/corpora/dbnl/description/dbnl.md index 9e73f9950..583046679 100644 --- a/backend/corpora/dbnl/description/dbnl.md +++ b/backend/corpora/dbnl/description/dbnl.md @@ -6,7 +6,7 @@ The Digital Library of Dutch Literature ([DBNL](https://www.dbnl.org/)) is a dig The DBNL dataset can be used for research into Dutch and Flemish linguistics and literature, from the middle ages to the present. Limburghish, Frisian, Surinam, and South African literature are represented. -The dataset contains digitised texts, which have been manually corrected, with metadata. It include medieval literature as well as classic novels. In addition, the dataset contains magazines from Dutch language and literary studies, such as De Gids and De Revisor. +The dataset contains digitised texts, which have been manually corrected, with metadata. It includes medieval literature as well as classic novels. In addition, the dataset contains magazines from Dutch language and literary studies, such as De Gids and De Revisor. ### Availability From ad35e42fb891e8136539c57b3e90e6a39713db23 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 13:29:06 +0200 Subject: [PATCH 191/262] update type of id field --- backend/corpora/dbnl/dbnl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index aefc15475..f6fbc0030 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -97,6 +97,7 @@ def _xml_files(self): id = Field( name='id', + es_mapping=keyword_mapping(), extractor=Combined( Metadata('id'), Index(transform=lambda i: str(i).zfill(4)), From 6f8529c1516588bf18aa293a3c064a26cae9f33b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 13:40:47 +0200 Subject: [PATCH 192/262] outfactor extractor check --- backend/addcorpus/corpus.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/backend/addcorpus/corpus.py b/backend/addcorpus/corpus.py index d18b47198..45e61aa7b 100644 --- a/backend/addcorpus/corpus.py +++ b/backend/addcorpus/corpus.py @@ -293,6 +293,15 @@ def documents(self, sources=None): ) ) + def _reject_extractors(self, *inapplicable_extractors): + ''' + Raise errors if any fields use extractors that are not applicable + for the corpus. + ''' + for field in self.fields: + if isinstance(field.extractor, inapplicable_extractors): + raise RuntimeError( + "Specified extractor method cannot be used with this type of data") class XMLCorpus(Corpus): ''' @@ -329,12 +338,8 @@ def source2dicts(self, source): default implementation for XML layouts; may be subclassed if more ''' # Make sure that extractors are sensible - for field in self.fields: - if isinstance(field.extractor, ( - extract.HTML, extract.CSV, - )): - raise RuntimeError( - "Specified extractor method cannot be used with an XML corpus") + self._reject_extractors(extract.HTML, extract.CSV) + # extract information from external xml files first, if applicable metadata = {} if isinstance(source, str): @@ -538,13 +543,7 @@ def source2dicts(self, source): ''' (filename, metadata) = source - # Make sure that extractors are sensible - for field in self.fields: - if isinstance(field.extractor, ( - extract.XML, extract.CSV, - )): - raise RuntimeError( - "Specified extractor method cannot be used with an HTML corpus") + self._reject_extractors(extract.XML, extract.CSV) # Loading HTML logger.info('Reading HTML file {} ...'.format(filename)) @@ -625,12 +624,7 @@ def skip_lines(self): def source2dicts(self, source): # make sure the field size is as big as the system permits csv.field_size_limit(sys.maxsize) - for field in self.fields: - if isinstance(field.extractor, ( - extract.HTML, extract.XML - )): - raise RuntimeError( - "Specified extractor method cannot be used with a CSV corpus") + self._reject_extractors(extract.XML, extract.HTML) if isinstance(source, str): filename = source From 2f576717eb388107ea1f136964e17f94c92a1237 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 13:43:39 +0200 Subject: [PATCH 193/262] rename index extractor --- backend/addcorpus/extract.py | 2 +- backend/corpora/dbnl/dbnl.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/addcorpus/extract.py b/backend/addcorpus/extract.py index efba228a1..fba462923 100644 --- a/backend/addcorpus/extract.py +++ b/backend/addcorpus/extract.py @@ -139,7 +139,7 @@ def __init__(self, extractor, *nargs, **kwargs): def _apply(self, *nargs, **kwargs): return self.extractor.apply(*nargs, **kwargs) -class Index(Extractor): +class Order(Extractor): def _apply(self, index=None, *nargs, **kwargs): return index diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index f6fbc0030..394a341ec 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -5,7 +5,7 @@ from django.conf import settings from addcorpus.corpus import XMLCorpus, Field -from addcorpus.extract import Metadata, XML, Pass, Index, Backup, Combined +from addcorpus.extract import Metadata, XML, Pass, Order, Backup, Combined import corpora.dbnl.utils as utils from addcorpus.es_mappings import * from addcorpus.filters import RangeFilter, MultipleChoiceFilter, BooleanFilter @@ -100,7 +100,7 @@ def _xml_files(self): es_mapping=keyword_mapping(), extractor=Combined( Metadata('id'), - Index(transform=lambda i: str(i).zfill(4)), + Order(transform=lambda i: str(i).zfill(4)), transform='_'.join, ) ) @@ -335,7 +335,7 @@ def _xml_files(self): name='chapter_index', display_name='Chapter index', description='Order of this chapter within the book', - extractor=Index( + extractor=Order( transform=lambda x : x + 1, applicable=lambda metadata: metadata['has_xml'] ), @@ -379,7 +379,7 @@ def _xml_files(self): name='is_primary', display_name='Primary', description='Whether this is the primary document for this book - each book has only one primary document', - extractor=Index(transform = lambda index : index == 0), + extractor=Order(transform = lambda index : index == 0), search_filter=BooleanFilter( true='Primary', false='Other', From af37a2f3a1c610e3ed775858558caeaae5e2b2ac Mon Sep 17 00:00:00 2001 From: Luka van der Plas <43678097+lukavdplas@users.noreply.github.com> Date: Wed, 7 Jun 2023 14:26:13 +0200 Subject: [PATCH 194/262] Update word models mock corpus docstring Co-authored-by: Berit --- backend/wordmodels/tests/mock-corpus/mock_corpus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/wordmodels/tests/mock-corpus/mock_corpus.py b/backend/wordmodels/tests/mock-corpus/mock_corpus.py index 7bdbb4c2d..a85c73eba 100644 --- a/backend/wordmodels/tests/mock-corpus/mock_corpus.py +++ b/backend/wordmodels/tests/mock-corpus/mock_corpus.py @@ -6,7 +6,7 @@ here = abspath(dirname(__file__)) class WordmodelsMockCorpus(Corpus): - title = "Mock corpus with SVD_PPMI models" + title = "Mock corpus with word models represented as Keyed Vectors" description = "Mock corpus for testing word models, saved as gensim Keyed Vectors" es_index = 'nothing' min_date = datetime.datetime(year=1810, month=1, day=1) From abc90e28f6003350b969a32697913579a9790482 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 7 Jun 2023 17:12:31 +0200 Subject: [PATCH 195/262] fix problems after merging --- backend/ianalyzer/config.py | 205 ++++++++++++++++++ .../mock-word-models/model_1810_1839.wv | Bin .../mock-word-models/model_1840_1869.wv | Bin .../mock-word-models/model_1870_1899.wv | Bin .../wordmodels/tests/test_related_words.py | 2 +- backend/wordmodels/tests/test_wm_import.py | 8 +- .../wordmodels/tests/test_wordmodels_views.py | 8 +- backend/wordmodels/urls.py | 2 +- backend/wordmodels/utils.py | 2 +- backend/wordmodels/views.py | 156 ++++++------- backend/wordmodels/visualisations.py | 8 +- .../src/app/services/wordmodels.service.ts | 2 +- 12 files changed, 286 insertions(+), 107 deletions(-) create mode 100644 backend/ianalyzer/config.py rename backend/wordmodels/tests/{ => mock-corpus}/mock-word-models/model_1810_1839.wv (100%) rename backend/wordmodels/tests/{ => mock-corpus}/mock-word-models/model_1840_1869.wv (100%) rename backend/wordmodels/tests/{ => mock-corpus}/mock-word-models/model_1870_1899.wv (100%) diff --git a/backend/ianalyzer/config.py b/backend/ianalyzer/config.py new file mode 100644 index 000000000..d879b996d --- /dev/null +++ b/backend/ianalyzer/config.py @@ -0,0 +1,205 @@ +''' +Configuration. +''' + +import logging +import csv +from os.path import expanduser, realpath, join, dirname, relpath +from datetime import datetime, timedelta + + +LOG_LEVEL = logging.INFO + +# Flask +DEBUG = True +TESTING = False +SECRET_KEY = '0987654321' +SECURITY_PASSWORD_SALT = '42istheanswertothelastofallquestions' +SECURITY_RECOVERABLE = True + +MAIL_SERVER = 'localhost' +MAIL_PORT = 25 +MAIL_USE_TLS = False +MAIL_USE_SSL = False +MAIL_USERNAME = '' +MAIL_PASSWORD = '' +MAIL_FROM_ADRESS = 'example@dhlab.nl' +MAIL_REGISTRATION_SUBJECT_LINE = 'Thank you for signing up at I-analyzer' +CSV_FILES_PATH = '/Users/janss089/git/ianalyzer/backend/api/csv_files' +BASE_URL = 'http://localhost:4200' +LOGO_LINK = 'http://dhstatic.hum.uu.nl/logo-lab/png/dighum-logo.png' + +# SQLAlchemy +SQLALCHEMY_DATABASE_URI = 'mysql://ianalyzer@localhost/ianalyzer' +SQLALCHEMY_TRACK_MODIFICATIONS = True + +ES_SEARCH_TIMEOUT = '30s' + +GOODREADS_DATA = '/Users/janss089/Desktop/Goodreads-2022' # only needed for indexing +GOODREADS_ES_INDEX = 'ianalyzer-goodreads' + +PEACEPORTAL_TOL_ES_INDEX = 'peaceportal-tol' +PEACEPORTAL_TOL_DATA = '/Users/janss089/DATA/peaceportal/TOL' + +PP_IRELAND_DATA = "some-path" +PP_IRELAND_INDEX = 'parliament-ireland' + +DUTCHNEWSPAPERS_ALL_DATA = '/Users/janss089/Desktop/DDD_000010100' +DUTCHNEWSPAPERS_ALL_ES_INDEX = 'ianalyzer-dutchnewspapers-all' + +# the corpora variable provides the file path of the corpus definition +# needs to be a full (not relative) file path +CORPORA = { + 'times': '/Users/janss089/git/ianalyzer/backend/corpora/times/times.py', + 'dutchnewspapers-public': '/Users/janss089/git/ianalyzer/backend/corpora/dutchnewspapers/dutchnewspapers_public.py', + 'dutchnewspapers-all': '/Users/janss089/git/ianalyzer/backend/corpora/dutchnewspapers/dutchnewspapers_all.py', + 'dutchannualreports': '/Users/janss089/git/ianalyzer/backend/corpora/dutchannualreports/dutchannualreports.py', + 'goodreads': '/Users/janss089/git/ianalyzer/backend/corpora/goodreads/goodreads.py', + 'troonredes': '/Users/janss089/git/ianalyzer/backend/corpora/troonredes/troonredes.py', + 'guardianobserver': '/Users/janss089/git/ianalyzer/backend/corpora/guardianobserver/guardianobserver.py', + 'periodicals': '/Users/janss089/git/ianalyzer/backend/corpora/periodicals/periodicals.py', + 'jewishinscriptions': '/Users/janss089/git/ianalyzer/backend/corpora/jewishinscriptions/jewishinscriptions.py', + 'ecco': '/Users/janss089/git/ianalyzer/backend/corpora/ecco/ecco.py', + 'parliament-netherlands': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/netherlands.py', + # 'parliament-norway': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/norway.py', + 'parliament-uk': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/uk.py', + # 'parliament-uk-recent': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/uk_recent.py', + # 'parliament-netherlands-recent': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/netherlands_recent.py' + # 'parliament': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/parliament.py' + 'parliament-france': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/france.py', + 'parliament-germany-new': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/germany-new.py', + 'parliament-ireland': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/ireland.py', + # 'dutchnewspapers-public': + # 'fiji': '/Users/janss089/git/ianalyzer/backend/corpora/peaceportal/FIJI/fiji.py', + # 'peaceportal': '/Users/janss089/git/ianalyzer/backend/corpora/peaceportal/peaceportal.py' + # 'tol': '/Users/janss089/git/ianalyzer/backend/corpora/peaceportal/tol.py' +} + +SERVERS = { + # Default ElasticSearch server + 'es8': { + 'host': 'localhost', + 'port': 9200, + 'api_id': 'AEIMRIEBauFysRDbdKAZ', + 'api_key': 'x5Fq2w5TQsGa2lbLBNVAgA', + 'certs_location': '/Applications/elasticsearch-8.0.0/config/certs/http_ca.crt', + 'chunk_size': 900, # Maximum number of documents sent during ES bulk operation + 'max_chunk_bytes': 1*1024*1024, # Maximum size of ES chunk during bulk operation + 'bulk_timeout': '60s', # Timeout of ES bulk operation + 'overview_query_size': 20, # Number of results to appear in the overview query + 'scroll_timeout': '3m', # Time before scroll results time out + 'scroll_page_size': 5000, # Number of results per scroll page + }, + 'default': { + 'host': 'localhost', + 'port': 9200, + 'chunk_size': 900, # Maximum number of documents sent during ES bulk operation + 'max_chunk_bytes': 1*1024*1024, # Maximum size of ES chunk during bulk operation + 'bulk_timeout': '60s', # Timeout of ES bulk operation + 'overview_query_size': 20, # Number of results to appear in the overview query + 'scroll_timeout': '3m', # Time before scroll results time out + 'scroll_page_size': 5000, + } +} + +SSL_CERT = '' + +# Index configurations +TIMES_ES_INDEX = 'times' +TIMES_ES_DOCTYPE = 'article' +TIMES_DATA = '/Users/janss089/DATA/times_test' + +DUTCHANNUALREPORTS_ES_INDEX = 'dutchannualreports' +DUTCHANNUALREPORTS_DATA = '/Users/janss089/DATA/NewDutchBanking' +DUTCHANNUALREPORTS_WM = '/Users/janss089/git/wordmodels/dutchannual' + +# DUTCHNEWSPAPERS_ALL_ES_INDEX = 'dutchnewspapers-all' +# DUTCHNEWSPAPERS_ALL_DATA = '/Users/janss089/DATA/kranten-delpher' + +DUTCHNEWSPAPERS_ES_INDEX = 'dutchnewspapers-public' +DUTCHNEWSPAPERS_ES_DOCTYPE = 'article' +DUTCHNEWSPAPERS_DATA = '/Users/janss089/DATA/kranten_test' +DUTCHNEWSPAPERS_TITLE = 'Dutch Newspapers' +DUTCHNEWSPAPERS_DESCRIPTION = 'Freely available part of the Delpher corpus, 1618-1876' + +GO_ES_INDEX = 'guardianobserver' +GO_ES_DOCTYPE = 'article' +GO_DATA = '/Users/janss089/DATA/guardian' + +TROONREDES_DATA = '/Users/janss089/DATA/troonredes' +TROONREDES_ES_INDEX = 'troonredes' +TROONREDES_ES_DOCTYPE = 'speech' + +PERIODICALS_DATA = '/Users/janss089/DATA/19thCenturyPeriodicals' +PERIODICALS_ES_INDEX = 'periodicals' +PERIODICALS_ES_DOCTYPE = 'article' + +JEWISH_INSCRIPTIONS_DATA = '/Users/janss089/DATA/jewish-inscriptions' +JEWISH_INSCRIPTIONS_ES_INDEX = 'jewishinscriptions' +JEWISH_INSCRIPTIONS_ES_DOCTYPE = 'epitaph' + +ECCO_DATA = '/Users/janss089/DATA/ecco' +ECCO_ES_INDEX = 'ecco' +ECCO_ES_DOCTYPE = 'page' + +PEACEPORTAL_FIJI_DATA = '/Users/janss089/DATA/peaceportal/FIJI' +PEACEPORTAL_FIJI_ES_INDEX = 'peaceportal-fiji' +PEACEPORTAL_ALIAS = 'peaceportal' + +PP_ALIAS = 'parliament' +PP_UK_DATA = '/Users/janss089/DATA/PeopleParliament/' +PP_UK_RECENT_DATA = '/Users/janss089/Desktop/RecentNL/ParlaMint-NL/ParlaMint-NL.TEI' +PP_UK_INDEX = 'parliament-uk' + +PP_NO_INDEX = 'parliament-norway' +PP_NO_DATA = '/Users/janss089/DATA/PeopleParliament/Norway' + +PP_NL_INDEX = 'parliament-netherlands' +PP_NL_DATA = '/Users/janss089/Desktop/uncompressed/d/nl/proc' +# PP_NL_RECENT_DATA = '/Users/janss089/Desktop/ParlaMint-NL/ParlaMint-NL.TEI' + + +PP_FR_INDEX = 'parliament-france' +# PP_FR_DATA = '/Users/janss089/Desktop/test' +PP_FR_DATA = '/Volumes/data/GW/CDH/DHLab/PeopleandParliament/ExampleData/France/' +PP_FR_WM = '/Users/janss089/Desktop/wm-france/france' + +PP_GERMANY_NEW_DATA = '/Volumes/data/GW/CDH/DHLab/PeopleandParliament/ExampleData/Germany-tiny/' +PP_GERMANY_NEW_INDEX = 'parliament-germany-new' + +PP_UK_WM = '/Users/janss089/Desktop/uk-lem-sep' + +# TROONREDES_WM = '/Users/janss089/git/wordmodels/troonredes' + +PP_ES_SETTINGS = { + "analysis": { + "analyzer": { + "clean": { + "tokenizer": "standard", + "char_filter": ["number_filter"], + "filter": ["lowercase", "stopwords"] + }, + "stemmed": { + "tokenizer": "standard", + "char_filter": ["number_filter"], + "filter": ["lowercase", "stopwords", "stemmer"] + } + }, + "char_filter":{ + "number_filter":{ + "type":"pattern_replace", + "pattern":"\\d+", + "replacement":"" + } + } + } +} + +# SAML +SAML_FOLDER = "/Users/janss089/git/ianalyzer/saml" +SAML_SOLISID_KEY = "uuShortID" +SAML_MAIL_KEY = "mail" + + +CORPUS_SERVER_NAMES = { +} diff --git a/backend/wordmodels/tests/mock-word-models/model_1810_1839.wv b/backend/wordmodels/tests/mock-corpus/mock-word-models/model_1810_1839.wv similarity index 100% rename from backend/wordmodels/tests/mock-word-models/model_1810_1839.wv rename to backend/wordmodels/tests/mock-corpus/mock-word-models/model_1810_1839.wv diff --git a/backend/wordmodels/tests/mock-word-models/model_1840_1869.wv b/backend/wordmodels/tests/mock-corpus/mock-word-models/model_1840_1869.wv similarity index 100% rename from backend/wordmodels/tests/mock-word-models/model_1840_1869.wv rename to backend/wordmodels/tests/mock-corpus/mock-word-models/model_1840_1869.wv diff --git a/backend/wordmodels/tests/mock-word-models/model_1870_1899.wv b/backend/wordmodels/tests/mock-corpus/mock-word-models/model_1870_1899.wv similarity index 100% rename from backend/wordmodels/tests/mock-word-models/model_1870_1899.wv rename to backend/wordmodels/tests/mock-corpus/mock-word-models/model_1870_1899.wv diff --git a/backend/wordmodels/tests/test_related_words.py b/backend/wordmodels/tests/test_related_words.py index 148206520..831b438e4 100644 --- a/backend/wordmodels/tests/test_related_words.py +++ b/backend/wordmodels/tests/test_related_words.py @@ -51,7 +51,7 @@ def test_context_time_interval(mock_corpus): most_similar_term = sorted_by_similarity[0]['key'] assert most_similar_term == case.get('similar1') -def test_diachronic_context(test_app, mock_corpus): +def test_diachronic_context(mock_corpus): word_data, times, _ = get_diachronic_contexts('she', mock_corpus) for item in word_data: diff --git a/backend/wordmodels/tests/test_wm_import.py b/backend/wordmodels/tests/test_wm_import.py index 78e416881..edf9505ad 100644 --- a/backend/wordmodels/tests/test_wm_import.py +++ b/backend/wordmodels/tests/test_wm_import.py @@ -3,11 +3,11 @@ import pytest from addcorpus.load_corpus import load_corpus -from wordmodels.utils import load_word_models, word_in_models, transform_query +from wordmodels.utils import load_word_models, word_in_modelss, transform_query from wordmodels.conftest import TEST_VOCAB_SIZE, TEST_DIMENSIONS, TEST_BINS from wordmodels.utils import load_wm_documentation -def test_import(test_app, mock_corpus): +def test_import(mock_corpus): corpus = load_corpus(mock_corpus) models = load_word_models(corpus) assert len(models) == len(TEST_BINS) @@ -24,7 +24,7 @@ def test_import(test_app, mock_corpus): vocab = weights.index_to_key assert len(vocab) == TEST_VOCAB_SIZE -def test_word_in_models(test_app, mock_corpus): +def test_word_in_models(mock_corpus): cases = [ { 'term': 'she', @@ -46,7 +46,7 @@ def test_word_in_models(test_app, mock_corpus): for case in cases: corpus = load_corpus(mock_corpus) - result = word_in_models(case['term'], corpus, 1) + result = word_in_modelss(case['term'], corpus, 1) assert result == case['expected'] def test_description_import(mock_corpus): diff --git a/backend/wordmodels/tests/test_wordmodels_views.py b/backend/wordmodels/tests/test_wordmodels_views.py index a2be98ae8..1603f84cc 100644 --- a/backend/wordmodels/tests/test_wordmodels_views.py +++ b/backend/wordmodels/tests/test_wordmodels_views.py @@ -34,16 +34,16 @@ def test_wm_documentation_view(authenticated_client, mock_corpus): assert 'documentation' in data assert data['documentation'] == 'Description for testing.\n' -word_in_model_test_cases = [ +word_in_models_test_cases = [ ('alice', True), ('Alice', True), ('aalice', False), ] -@pytest.mark.parametrize('term,in_model', word_in_model_test_cases) -def test_word_in_model_view(term, in_model, authenticated_client, mock_corpus): +@pytest.mark.parametrize('term,in_model', word_in_models_test_cases) +def test_word_in_models_view(term, in_model, authenticated_client, mock_corpus): response = authenticated_client.get( - f'/api/wordmodels/word_in_model?query_term={term}&corpus_name={mock_corpus}', + f'/api/wordmodels/word_in_models?query_term={term}&corpus_name={mock_corpus}', content_type='application/json' ) assert response.status_code == 200 diff --git a/backend/wordmodels/urls.py b/backend/wordmodels/urls.py index 3fa6d1db0..876df2d80 100644 --- a/backend/wordmodels/urls.py +++ b/backend/wordmodels/urls.py @@ -5,5 +5,5 @@ path('related_words', RelatedWordsView.as_view()), path('similarity_over_time', SimilarityView.as_view()), path('documentation', DocumentationView.as_view()), - path('word_in_model', WordInModelView.as_view()), + path('word_in_models', WordInModelView.as_view()), ] diff --git a/backend/wordmodels/utils.py b/backend/wordmodels/utils.py index 32ee61895..addc36cb5 100644 --- a/backend/wordmodels/utils.py +++ b/backend/wordmodels/utils.py @@ -28,7 +28,7 @@ def load_word_models(corpus): def get_year(kv_filename, position): return int(splitext(basename(kv_filename))[0].split('_')[position]) -def word_in_models(query_term, corpus, max_distance=2): +def word_in_modelss(query_term, corpus, max_distance=2): models = load_word_models(corpus) transformed_query = transform_query(query_term) vocab = set() diff --git a/backend/wordmodels/views.py b/backend/wordmodels/views.py index 73f353dbb..047a25de3 100644 --- a/backend/wordmodels/views.py +++ b/backend/wordmodels/views.py @@ -1,92 +1,68 @@ -from flask import request, abort, current_app, jsonify, Blueprint -from flask_login import LoginManager, login_required -import wordmodels.visualisations as visualisations -import wordmodels.utils as utils - -wordmodels = Blueprint('wordmodels', __name__) - - -@wordmodels.route('/get_related_words', methods=['POST']) -@login_required -def get_related_words(): - if not request.json: - abort(400) - results = visualisations.get_diachronic_contexts( - request.json['query_term'], - request.json['corpus_name'], - number_similar = request.json.get('neighbours'), - ) - if isinstance(results, str): - # the method returned an error string - response = jsonify({ - 'success': False, - 'message': results}) - else: - response = jsonify({ - 'success': True, - 'data': { - 'similarities_over_time': results[0], - 'similarities_over_time_local_top_n': results[2], - 'time_points': results[1] - } - }) - return response - - -@wordmodels.route('/get_similarity_over_time', methods=['GET']) -@login_required -def get_similarity(): - if not request.args: - abort(400) - results = visualisations.get_similarity_over_time( - request.args['term_1'], - request.args['term_2'], - request.args['corpus_name'] - ) - if isinstance(results, str): - # the method returned an error string - response = jsonify({ - 'success': False, - 'message': results}) - else: - response = jsonify({ - 'success': True, - 'data': results - }) - return response - -@wordmodels.route('/get_wm_documentation', methods=['GET']) -@login_required -def get_word_models_documentation(): - if not request.args and 'corpus_name' in request.args: - abort(400) - - corpus = request.args['corpus_name'] - documentation = utils.load_wm_documentation(corpus) - - return { - 'documentation': documentation - } - - -@wordmodels.route('/get_word_in_model', methods=['GET']) -@login_required -def get_word_in_model(): - if not request.args: - abort(400) - results = utils.word_in_models( - request.args['query_term'], - request.args['corpus_name'] - ) - if isinstance(results, str): - # the method returned an error string - response = jsonify({ - 'success': False, - 'message': results}) - else: - response = jsonify({ - 'success': True, - 'result': results +from django.shortcuts import render +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from addcorpus.permissions import CorpusAccessPermission, corpus_name_from_request +from wordmodels import utils, visualisations +from rest_framework.exceptions import APIException + +class RelatedWordsView(APIView): + ''' + Get words with the highest similarity to the query term + ''' + + permission_classes = [IsAuthenticated, CorpusAccessPermission] + + def post(self, request, *args, **kwargs): + corpus = corpus_name_from_request(request) + results = visualisations.get_diachronic_contexts( + request.data['query_term'], + corpus, + number_similar = request.data['neighbours'], + ) + if isinstance(results, str): + # the method returned an error string + raise APIException(detail=results) + else: + return Response({ + 'similarities_over_time': results[0], + 'similarities_over_time_local_top_n': results[1], + 'time_points': results[1] + }) + +class SimilarityView(APIView): + ''' + Get similarity between two query terms + ''' + + permission_classes = [IsAuthenticated, CorpusAccessPermission] + + def get(self, request, *args, **kwargs): + corpus = corpus_name_from_request(request) + term_1 = request.query_params.get('term_1') + term_2 = request.query_params.get('term_2') + + results = visualisations.get_similarity_over_time(term_1, term_2, corpus) + + if isinstance(results, str): + # the method returned an error string + raise APIException(detail=results) + else: + return Response(results) + +class DocumentationView(APIView): + ''' + Get word models documentation for a corpus + ''' + + permission_classes = [IsAuthenticated, CorpusAccessPermission] + + def get(self, request, *args, **kwargs): + corpus = corpus_name_from_request(request) + documentation = utils.load_wm_documentation(corpus) + + return Response({ + 'documentation': documentation }) class WordInModelView(APIView): @@ -100,7 +76,7 @@ def get(self, request, *args, **kwargs): corpus = corpus_name_from_request(request) query_term = request.query_params.get('query_term') - results = utils.word_in_model(query_term, corpus) + results = utils.word_in_modelss(query_term, corpus) if isinstance(results, str): # the method returned an error string diff --git a/backend/wordmodels/visualisations.py b/backend/wordmodels/visualisations.py index fc9f46f34..8c4827f38 100644 --- a/backend/wordmodels/visualisations.py +++ b/backend/wordmodels/visualisations.py @@ -3,8 +3,6 @@ import numpy as np import pandas as pd -from flask import current_app - from addcorpus.load_corpus import load_corpus from wordmodels.similarity import find_n_most_similar, term_similarity from wordmodels.utils import load_word_models @@ -48,7 +46,7 @@ def get_diachronic_contexts(query_term, corpus_string, number_similar=NUMBER_SIM wm_list = load_word_models(corpus) times = get_time_labels(wm_list) data_per_timeframe = [ - find_n_most_similar(time_bin, query_term, number_similar*2) + find_n_most_similar(time_bin, query_term, number_similar) for time_bin in wm_list ] flattened_data = reduce(concat, data_per_timeframe) @@ -56,8 +54,8 @@ def get_diachronic_contexts(query_term, corpus_string, number_similar=NUMBER_SIM frequencies = {word: [] for word in all_words} for item in flattened_data: frequencies[item['key']].append(item['similarity']) - means = pd.DataFrame({'word': all_words, 'mean': [np.mean(f) for f in frequencies.values()]}) - words = means.nlargest(number_similar, 'mean')['word'] + max_similarities = pd.DataFrame({'word': all_words, 'max': [max(f) for f in frequencies.values()]}) + words = max_similarities.nlargest(number_similar, 'max')['word'] get_similarity = lambda word, time_bin: term_similarity( time_bin, diff --git a/frontend/src/app/services/wordmodels.service.ts b/frontend/src/app/services/wordmodels.service.ts index c8a430033..e6918707e 100644 --- a/frontend/src/app/services/wordmodels.service.ts +++ b/frontend/src/app/services/wordmodels.service.ts @@ -37,7 +37,7 @@ export class WordmodelsService extends Resource { @ResourceAction({ method: ResourceRequestMethod.Get, - path: '/word_in_model' + path: '/word_in_models' }) public wordInModelRequest: ResourceMethod< { query_term: string; corpus_name: string }, From cda25bf5d3df50272f71340ef45fdf1d3b1ac694 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 17:24:56 +0200 Subject: [PATCH 196/262] reset parameters when switching corpus --- frontend/src/app/search/search.component.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index 739a6d223..ea3bfe52b 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -6,6 +6,7 @@ import { Corpus, CorpusField, ResultOverview, QueryModel, User } from '../models import { CorpusService, DialogService, } from '../services/index'; import { ParamDirective } from '../param/param-directive'; import { AuthService } from '../services/auth.service'; +import * as _ from 'lodash'; @Component({ selector: 'ia-search', @@ -119,14 +120,17 @@ export class SearchComponent extends ParamDirective { private setCorpus(corpus: Corpus) { if (!this.corpus || this.corpus.name !== corpus.name) { + const reset = !_.isUndefined(this.corpus); this.corpus = corpus; - this.setQueryModel(); + this.setQueryModel(reset); } } - private setQueryModel() { + private setQueryModel(reset: boolean) { const queryModel = new QueryModel(this.corpus); - queryModel.setFromParams(this.route.snapshot.queryParamMap); + if (!reset) { + queryModel.setFromParams(this.route.snapshot.queryParamMap); + } this.queryModel = queryModel; this.queryText = queryModel.queryText; this.queryModel.update.subscribe(() => { From 1b29a9719e10fb960e9fd58e31ed37dd1dd2fb63 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 7 Jun 2023 17:56:05 +0200 Subject: [PATCH 197/262] send correct data to frontend --- backend/wordmodels/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/wordmodels/views.py b/backend/wordmodels/views.py index 047a25de3..5519e42f8 100644 --- a/backend/wordmodels/views.py +++ b/backend/wordmodels/views.py @@ -26,7 +26,7 @@ def post(self, request, *args, **kwargs): else: return Response({ 'similarities_over_time': results[0], - 'similarities_over_time_local_top_n': results[1], + 'similarities_over_time_local_top_n': results[2], 'time_points': results[1] }) From cca5f11422fa73ef56c7e39a7f1267989756358d Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 8 Jun 2023 09:53:05 +0200 Subject: [PATCH 198/262] change typo --- backend/wordmodels/tests/test_wm_import.py | 4 ++-- backend/wordmodels/utils.py | 2 +- backend/wordmodels/views.py | 2 +- backend/wordmodels/visualisations.py | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/wordmodels/tests/test_wm_import.py b/backend/wordmodels/tests/test_wm_import.py index edf9505ad..e6a36b467 100644 --- a/backend/wordmodels/tests/test_wm_import.py +++ b/backend/wordmodels/tests/test_wm_import.py @@ -3,7 +3,7 @@ import pytest from addcorpus.load_corpus import load_corpus -from wordmodels.utils import load_word_models, word_in_modelss, transform_query +from wordmodels.utils import load_word_models, word_in_models, transform_query from wordmodels.conftest import TEST_VOCAB_SIZE, TEST_DIMENSIONS, TEST_BINS from wordmodels.utils import load_wm_documentation @@ -46,7 +46,7 @@ def test_word_in_models(mock_corpus): for case in cases: corpus = load_corpus(mock_corpus) - result = word_in_modelss(case['term'], corpus, 1) + result = word_in_models(case['term'], corpus, 1) assert result == case['expected'] def test_description_import(mock_corpus): diff --git a/backend/wordmodels/utils.py b/backend/wordmodels/utils.py index addc36cb5..32ee61895 100644 --- a/backend/wordmodels/utils.py +++ b/backend/wordmodels/utils.py @@ -28,7 +28,7 @@ def load_word_models(corpus): def get_year(kv_filename, position): return int(splitext(basename(kv_filename))[0].split('_')[position]) -def word_in_modelss(query_term, corpus, max_distance=2): +def word_in_models(query_term, corpus, max_distance=2): models = load_word_models(corpus) transformed_query = transform_query(query_term) vocab = set() diff --git a/backend/wordmodels/views.py b/backend/wordmodels/views.py index 5519e42f8..c564f7561 100644 --- a/backend/wordmodels/views.py +++ b/backend/wordmodels/views.py @@ -76,7 +76,7 @@ def get(self, request, *args, **kwargs): corpus = corpus_name_from_request(request) query_term = request.query_params.get('query_term') - results = utils.word_in_modelss(query_term, corpus) + results = utils.word_in_models(query_term, corpus) if isinstance(results, str): # the method returned an error string diff --git a/backend/wordmodels/visualisations.py b/backend/wordmodels/visualisations.py index 8c4827f38..4eb865f80 100644 --- a/backend/wordmodels/visualisations.py +++ b/backend/wordmodels/visualisations.py @@ -1,6 +1,5 @@ from functools import reduce from operator import concat -import numpy as np import pandas as pd from addcorpus.load_corpus import load_corpus From a728c79d3fab4bf7efd1542cd4f5241aac6be216 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 8 Jun 2023 13:44:08 +0200 Subject: [PATCH 199/262] non-equidistant spacing between x axis labels --- .../similarity-chart/similarity-chart.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts index 693a1a1dd..5ac8596c9 100644 --- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts +++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts @@ -111,7 +111,7 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { const allSeries = _.groupBy(data, point => point.key); const datasets = _.values(allSeries).map((series, datasetIndex) => { const label = series[0].key; - const similarities = series.map(point => point.similarity); + const similarities = series.map(point => ({x: Number(point.time.split('-')[0]), y: point.similarity})); const colour = selectColor(this.palette, datasetIndex); return { label, @@ -166,7 +166,12 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { }, }, scales: { - x: {}, + x: { + type: 'linear', + ticks: { + callback: (value) => Number(value) + } + }, y: { title: { display: true, From dc4b9a2e23781a07f24a75c74ad1e7eaa647252f Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 8 Jun 2023 14:38:43 +0200 Subject: [PATCH 200/262] fix tests --- .../app/download/download.component.spec.ts | 18 +++++++++--------- .../filter/filter-manager.component.spec.ts | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/download/download.component.spec.ts b/frontend/src/app/download/download.component.spec.ts index f31f74923..fcdfd2b3a 100644 --- a/frontend/src/app/download/download.component.spec.ts +++ b/frontend/src/app/download/download.component.spec.ts @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { commonTestBed } from '../common-test-bed'; -import { CorpusField } from "../models"; -import { mockCorpus, mockField, mockField2 } from "../../mock-data/corpus"; +import { CorpusField } from '../models'; +import { mockCorpus, mockField, mockField2 } from '../../mock-data/corpus'; -import { DownloadComponent } from "./download.component"; +import { DownloadComponent } from './download.component'; -describe("DownloadComponent", () => { +describe('DownloadComponent', () => { let component: DownloadComponent; let fixture: ComponentFixture; @@ -21,22 +21,22 @@ describe("DownloadComponent", () => { fixture.detectChanges(); }); - it("should create", () => { + it('should create', () => { expect(component).toBeTruthy(); }); - it("should respond to field selection", () => { + it('should respond to field selection', () => { // Start with a single field - expect(component["getCsvFields"]()).toEqual([mockField]); + expect(component['getCsvFields']()).toEqual(mockCorpus.fields); // Deselect all component.selectCsvFields([]); - expect(component["getCsvFields"]()).toEqual([]); + expect(component['getCsvFields']()).toEqual([]); // Select two component.selectCsvFields([mockField, mockField2]); const expected_fields = [mockField, mockField2]; - expect(component["getCsvFields"]()).toEqual(expected_fields); + expect(component['getCsvFields']()).toEqual(expected_fields); expect(component.selectedCsvFields).toEqual(expected_fields); }); }); diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index b5a3bcab9..4443e20c5 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -24,7 +24,7 @@ describe('FilterManagerComponent', () => { it('should create', () => { expect(component).toBeTruthy(); - expect(component.potentialFilters.length).toEqual(1); + expect(component.potentialFilters.length).toEqual(2); }); it('resets filters when corpus changes', () => { @@ -37,7 +37,7 @@ describe('FilterManagerComponent', () => { component.corpus = mockCorpus; component.queryModel = new QueryModel(mockCorpus); fixture.detectChanges(); - expect(component.potentialFilters.length).toEqual(1); + expect(component.potentialFilters.length).toEqual(2); expect(component.potentialFilters[0].adHoc).toBeFalse(); }); From f9880651814ba22b7cefee8897af74fe61116cf1 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 8 Jun 2023 14:40:18 +0200 Subject: [PATCH 201/262] extra docstrings --- frontend/src/app/models/query.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 7e0143516..d241536d1 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -148,8 +148,13 @@ export class QueryModel { return newQuery; } - /** convert the query to a parameter map */ - toRouteParam(): {[param: string]: any} { + /** + * convert the query to a parameter map + * + * All query-related params are explicity listed; + * empty parameters have value null. + */ + toRouteParam(): {[param: string]: string|null} { const queryTextParams = { query: this.queryText || null }; const searchFieldsParams = { fields: this.searchFields?.map(f => f.name).join(',') || null}; const sortParams = this.sort.toRouteParam(); @@ -165,7 +170,13 @@ export class QueryModel { }; } - toQueryParams() { + /** + * convert the query to a a parameter map, only + * including properties that should actually be explicated + * in the route. Same as query.toRouteParam() but + * without null values. + */ + toQueryParams(): {[param: string]: string} { return omitNullParameters(this.toRouteParam()); } From 4bf4c20cb31a89d874aec3535469df324aca1977 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 8 Jun 2023 14:43:29 +0200 Subject: [PATCH 202/262] parameter change detection function --- frontend/src/app/utils/params.spec.ts | 31 +++++++++++++++++++++------ frontend/src/app/utils/params.ts | 8 +++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/utils/params.spec.ts b/frontend/src/app/utils/params.spec.ts index 2f4630e2b..a94d4ad85 100644 --- a/frontend/src/app/utils/params.spec.ts +++ b/frontend/src/app/utils/params.spec.ts @@ -1,6 +1,7 @@ import { convertToParamMap } from '@angular/router'; -import { highlightFromParams, omitNullParameters, searchFieldsFromParams } from './params'; -import { mockCorpus3, mockField2 } from '../../mock-data/corpus'; +import { highlightFromParams, omitNullParameters, paramsHaveChanged, searchFieldsFromParams } from './params'; +import { mockCorpus, mockCorpus3, mockField2 } from '../../mock-data/corpus'; +import { QueryModel } from '../models'; describe('searchFieldsFromParams', () => { it('should parse field parameters', () => { @@ -22,20 +23,38 @@ describe('highlightFromParams', () => { describe('omitNullParameters', () => { it('should omit null parameters', () => { - const p = { a: null, b: 1, c: 'test' }; + const p = { a: null, b: '1', c: 'test' }; expect(omitNullParameters(p)).toEqual( - { b: 1, c: 'test' } + { b: '1', c: 'test' } ); }); }); describe('omitNullParameters', () => { it('should omit null parameters', () => { - const p = { a: null, b: 1, c: 'test' }; + const p = { a: null, b: '1', c: 'test' }; expect(omitNullParameters(p)).toEqual( - { b: 1, c: 'test' } + { b: '1', c: 'test' } ); }); }); + +describe('paramsHaveChanged', () => { + it('should detect changes in parameters', () => { + const queryModel = new QueryModel(mockCorpus); + + const params1 = convertToParamMap({}); + const params2 = convertToParamMap({query: 'test'}); + + expect(paramsHaveChanged(queryModel, params1)).toBeFalse(); + expect(paramsHaveChanged(queryModel, params2)).toBeTrue(); + + queryModel.setFromParams(params2); + + expect(paramsHaveChanged(queryModel, params2)).toBeFalse(); + expect(paramsHaveChanged(queryModel, params1)).toBeTrue(); + + }); +}); diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 9d6ee3da7..961cd8930 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -63,3 +63,11 @@ export const queryFiltersToParams = (queryModel: QueryModel) => { {} ); }; + +export const paramsHaveChanged = (queryModel: QueryModel, newParams: ParamMap) => { + const currentParams = queryModel.toRouteParam(); + + return _.some(_.keys(currentParams).map(key => + newParams.get(key) !== currentParams[key] + )); +}; From 0cdaba7c67b96b2af15c485b73e6796ce0425ca8 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 8 Jun 2023 15:25:19 +0200 Subject: [PATCH 203/262] add test for single update when setting from params --- frontend/src/app/models/query.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index c95665568..66e270fe4 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -189,4 +189,20 @@ describe('QueryModel', () => { expect(query.filters[0].currentData.min).toEqual(new Date('Jan 2 1850')); expect(clone.filters[0].currentData.min).toEqual(new Date('Jan 1 1850')); }); + + it('should fire a single update event when updating from params', () => { + // dirty up the settings a bit + query.setQueryText('test'); + query.addFilter(filter); + query.sort.setSortBy(mockFieldDate); + query.sort.setSortDirection('desc'); + + let updates = 0; + query.update.subscribe(() => updates += 1); + + const params = convertToParamMap({}); + query.setFromParams(params); + expect(updates).toBe(1); + + }); }); From 45ad2ecf3f94c346d6b08072abf5cd6b112e906c Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 8 Jun 2023 17:06:58 +0200 Subject: [PATCH 204/262] fix x axis labels, bar charts and tooltips --- .../similarity-chart.component.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts index 5ac8596c9..7d3439140 100644 --- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts +++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; -import { Chart, ChartData, ChartOptions, Filler } from 'chart.js'; +import { Chart, ChartData, ChartOptions, Filler, TooltipItem } from 'chart.js'; import Zoom from 'chartjs-plugin-zoom'; import * as _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; @@ -31,6 +31,7 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { tableHeaders: FreqTableHeaders; tableData: WordSimilarity[]; + startTimeIntervals: Number[]; graphStyle = new BehaviorSubject<'line'|'bar'>('line'); @@ -106,12 +107,30 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { } } + formatLabel(value: number): string { + return this.timeIntervals.filter(t => t.startsWith(value.toString()))[0] + } + + getStartTime(time: string): number { + return parseInt(time.split('-')[0]) + } + + formatDataPoint(point: any, style: string): {x: number, y: number} | number { + if (style == 'line') { + return {x: parseInt(point.time.split('-')[0]), y: point.similarity} + } + else { + return point.similarity + } + } + /** convert array of word similarities to a chartData object */ makeChartData(data: WordSimilarity[], style: 'line'|'bar'): ChartData { + this.startTimeIntervals = this.timeIntervals.map(time => this.getStartTime(time)); const allSeries = _.groupBy(data, point => point.key); const datasets = _.values(allSeries).map((series, datasetIndex) => { const label = series[0].key; - const similarities = series.map(point => ({x: Number(point.time.split('-')[0]), y: point.similarity})); + const similarities = series.map((point) => this.formatDataPoint(point, style)); const colour = selectColor(this.palette, datasetIndex); return { label, @@ -169,7 +188,10 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { x: { type: 'linear', ticks: { - callback: (value) => Number(value) + stepSize: 1, + autoSkip: true, + callback: (value: number): string | undefined => this.startTimeIntervals.includes(value) ? this.formatLabel(value) : undefined, + minRotation: 30 } }, y: { @@ -187,6 +209,9 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { tooltip: { displayColors: true, callbacks: { + title: (tooltipItem: any): string => { + return this.formatLabel(tooltipItem[0].parsed.x) + }, labelColor: (tooltipItem: any): any => { const color = tooltipItem.dataset.borderColor; return { From 0527ccb26e68d6875644e6fb0c7f0192980a5800 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 09:49:00 +0200 Subject: [PATCH 205/262] no subscription for sort updates --- frontend/src/app/models/query.ts | 17 ++++++++++++++--- .../src/app/search/search-sorting.component.ts | 4 ++-- frontend/src/mock-data/search.ts | 4 ++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index d241536d1..cde92bc09 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,7 +1,7 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; import { combineLatest, Subject, Subscription } from 'rxjs'; -import { Corpus, CorpusField, SortConfiguration, } from '../models/index'; +import { Corpus, CorpusField, SortBy, SortConfiguration, SortDirection, } from '../models/index'; import { EsQuery } from '../services'; import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; import { @@ -84,8 +84,7 @@ export class QueryModel { constructor(corpus: Corpus) { this.corpus = corpus; this.sort = new SortConfiguration(this.corpus); - this.sort.configuration$.subscribe(() => this.update.next()); - } + } setQueryText(text?: string) { this.queryText = text || undefined; @@ -97,6 +96,18 @@ export class QueryModel { this.subscribeToFilterUpdates(); } + + setSortBy(value: SortBy) { + this.sort.setSortBy(value); + this.update.next(); + } + + setSortDirection(value: SortDirection) { + this.sort.setSortDirection(value); + this.update.next(); + } + + removeFilter(filter: SearchFilter) { this.removeFiltersForField(filter.corpusField); } diff --git a/frontend/src/app/search/search-sorting.component.ts b/frontend/src/app/search/search-sorting.component.ts index a23024314..42a6beaf6 100644 --- a/frontend/src/app/search/search-sorting.component.ts +++ b/frontend/src/app/search/search-sorting.component.ts @@ -52,7 +52,7 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { public toggleSortType() { const direction = this.ascending ? 'desc' : 'asc'; - this.sortConfiguration.setSortDirection(direction); + this.queryModel.setSortDirection(direction); } public toggleShowFields() { @@ -65,7 +65,7 @@ export class SearchSortingComponent implements OnChanges, OnDestroy { } else { this.valueType = ['integer', 'date', 'boolean'].indexOf(field.displayType) >= 0 ? 'numeric' : 'alpha'; } - this.sortConfiguration.setSortBy(field || undefined); + this.queryModel.setSortBy(field || undefined); } } diff --git a/frontend/src/mock-data/search.ts b/frontend/src/mock-data/search.ts index af9b0406d..c01a86239 100644 --- a/frontend/src/mock-data/search.ts +++ b/frontend/src/mock-data/search.ts @@ -34,8 +34,8 @@ export class SearchServiceMock { filters.forEach(model.addFilter); if (sortField) { - model.sort.setSortBy(sortField); - model.sort.setSortDirection(sortAscending ? 'asc' : 'desc'); + model.setSortBy(sortField); + model.setSortDirection(sortAscending ? 'asc' : 'desc'); } model.highlightSize = highlight; From b5b6ac62ebdc5e949c52d5b080a7f2ef7ac3f80d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 09:54:02 +0200 Subject: [PATCH 206/262] only run update from params if they are different --- frontend/src/app/models/query.spec.ts | 18 ++++++++++++++++++ frontend/src/app/models/query.ts | 16 +++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 66e270fe4..d02c6bec5 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -203,6 +203,24 @@ describe('QueryModel', () => { const params = convertToParamMap({}); query.setFromParams(params); expect(updates).toBe(1); + }); + + it('should not fire updates when params are unchanged', () => { + let updates = 0; + query.update.subscribe(() => updates += 1); + + const emptyParams = convertToParamMap({}); + + query.setFromParams(emptyParams); + + expect(updates).toBe(0); + + const params1 = convertToParamMap({ + query: 'test' + }); + query.setFromParams(params1); + + expect(updates).toBe(1); }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index cde92bc09..1fdfa3b98 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -5,7 +5,7 @@ import { Corpus, CorpusField, SortBy, SortConfiguration, SortDirection, } from ' import { EsQuery } from '../services'; import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; import { - filtersFromParams, highlightFromParams, omitNullParameters, queryFiltersToParams, + filtersFromParams, highlightFromParams, omitNullParameters, paramsHaveChanged, queryFiltersToParams, queryFromParams, searchFieldsFromParams } from '../utils/params'; import { SearchFilter } from './search-filter'; @@ -134,12 +134,14 @@ export class QueryModel { /** set the query values from a parameter map */ setFromParams(params: ParamMap) { - this.queryText = queryFromParams(params); - this.searchFields = searchFieldsFromParams(params, this.corpus); - filtersFromParams(params, this.corpus).forEach(filter => this.addFilter(filter)); - this.sort.setFromParams(params); - this.highlightSize = highlightFromParams(params); - this.update.next(); + if (paramsHaveChanged(this, params)) { + this.queryText = queryFromParams(params); + this.searchFields = searchFieldsFromParams(params, this.corpus); + filtersFromParams(params, this.corpus).forEach(filter => this.addFilter(filter)); + this.sort.setFromParams(params); + this.highlightSize = highlightFromParams(params); + this.update.next(); + } } /** From 4cfe302e0cf9483a5f9518be8b5c94befb33d3f7 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 10:08:49 +0200 Subject: [PATCH 207/262] extra unit test for params change detection --- frontend/src/app/utils/params.spec.ts | 21 +++++++++++++++++---- frontend/src/app/utils/params.ts | 4 ++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/utils/params.spec.ts b/frontend/src/app/utils/params.spec.ts index a94d4ad85..3a72f93ae 100644 --- a/frontend/src/app/utils/params.spec.ts +++ b/frontend/src/app/utils/params.spec.ts @@ -1,7 +1,7 @@ import { convertToParamMap } from '@angular/router'; import { highlightFromParams, omitNullParameters, paramsHaveChanged, searchFieldsFromParams } from './params'; -import { mockCorpus, mockCorpus3, mockField2 } from '../../mock-data/corpus'; -import { QueryModel } from '../models'; +import { mockCorpus, mockCorpus3, mockField2, mockField } from '../../mock-data/corpus'; +import { MultipleChoiceFilter, QueryModel } from '../models'; describe('searchFieldsFromParams', () => { it('should parse field parameters', () => { @@ -42,9 +42,14 @@ describe('omitNullParameters', () => { }); describe('paramsHaveChanged', () => { - it('should detect changes in parameters', () => { - const queryModel = new QueryModel(mockCorpus); + const corpus = mockCorpus; + let queryModel: QueryModel; + + beforeEach(() => { + queryModel = new QueryModel(corpus); + }); + it('should detect changes in parameters', () => { const params1 = convertToParamMap({}); const params2 = convertToParamMap({query: 'test'}); @@ -57,4 +62,12 @@ describe('paramsHaveChanged', () => { expect(paramsHaveChanged(queryModel, params1)).toBeTrue(); }); + + it('should detect new filters', () => { + const filter = mockField.makeSearchFilter() as MultipleChoiceFilter; + filter.data.next(['test']); + const params = convertToParamMap(filter.toRouteParam()); + + expect(paramsHaveChanged(queryModel, params)).toBeTrue(); + }); }); diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index 961cd8930..a2dd03a32 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -67,7 +67,7 @@ export const queryFiltersToParams = (queryModel: QueryModel) => { export const paramsHaveChanged = (queryModel: QueryModel, newParams: ParamMap) => { const currentParams = queryModel.toRouteParam(); - return _.some(_.keys(currentParams).map(key => + return _.some( _.keys(currentParams), key => newParams.get(key) !== currentParams[key] - )); + ); }; From 4aa90fcfdc69596c6972cccfaa2050f07eb6314d Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 10:17:04 +0200 Subject: [PATCH 208/262] filter update logic --- frontend/src/app/models/query.spec.ts | 11 ++++++++++- frontend/src/app/models/query.ts | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index d02c6bec5..abf254a3e 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -203,6 +203,16 @@ describe('QueryModel', () => { const params = convertToParamMap({}); query.setFromParams(params); expect(updates).toBe(1); + + const params2 = convertToParamMap({ + query: 'test', + ... filter.toRouteParam(), + ... filter2.toRouteParam(), + }); + + query.setFromParams(params2); + + expect(updates).toBe(2); }); it('should not fire updates when params are unchanged', () => { @@ -221,6 +231,5 @@ describe('QueryModel', () => { query.setFromParams(params1); expect(updates).toBe(1); - }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 1fdfa3b98..1f9187654 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -137,10 +137,10 @@ export class QueryModel { if (paramsHaveChanged(this, params)) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); - filtersFromParams(params, this.corpus).forEach(filter => this.addFilter(filter)); + this.filters = filtersFromParams(params, this.corpus); this.sort.setFromParams(params); this.highlightSize = highlightFromParams(params); - this.update.next(); + this.subscribeToFilterUpdates(); } } From 8130ca4f01aa67c90abe15ee112ff4e60df5eb33 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 10:21:55 +0200 Subject: [PATCH 209/262] update query model with parameter changes --- frontend/src/app/search/search.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index ea3bfe52b..ab671f732 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -74,6 +74,7 @@ export class SearchComponent extends ParamDirective { setStateFromParams(params: ParamMap) { this.tabIndex = params.has('visualize') ? 1 : 0; this.showVisualization = params.has('visualize') ? true : false; + this.queryModel.setFromParams(params); } @HostListener('window:scroll', []) From 5e3b86a0bfc876b5ea24d681c134b325b4bd0d41 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 10:44:41 +0200 Subject: [PATCH 210/262] implement activation logic on SearchFilter class --- frontend/src/app/models/search-filter.spec.ts | 54 ++++++++++++++++++- frontend/src/app/models/search-filter.ts | 46 ++++++++++++++-- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index e4f08f3d1..0896f408c 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -1,5 +1,57 @@ import { mockFieldMultipleChoice, mockFieldDate } from '../../mock-data/corpus'; -import { DateFilter, MultipleChoiceFilter } from './search-filter'; +import { DateFilter, DateFilterData, MultipleChoiceFilter } from './search-filter'; + +describe('SearchFilter', () => { + // while these tests are ran on the DateFilter, + // they test logic implemented in the abstract + // SearchFilter class + + const field = mockFieldDate; + let filter: DateFilter; + const exampleData: DateFilterData = { + min: new Date(Date.parse('Jan 01 1850')), + max: new Date(Date.parse('Dec 31 1860')) + }; + const isActive = () => filter.active.value; + + beforeEach(() => { + filter = new DateFilter(field); + }); + + it('should toggle', () => { + expect(isActive()).toBeFalse(); + + filter.toggle(); + expect(isActive()).toBeTrue(); + filter.toggle(); + expect(isActive()).toBeFalse(); + + filter.deactivate(); + expect(isActive()).toBeFalse(); + filter.activate(); + expect(isActive()).toBeTrue(); + filter.deactivate(); + expect(isActive()).toBeFalse(); + }); + + it('should activate when value is set to non-default', () => { + expect(isActive()).toBeFalse(); + + filter.set(filter.defaultData); + expect(isActive()).toBeFalse(); + + filter.set(exampleData); + expect(isActive()).toBeTrue(); + }); + + it('should deactivate when reset', () => { + filter.set(exampleData); + expect(isActive()).toBeTrue(); + + filter.reset(); + expect(isActive()).toBeFalse(); + }); +}); describe('DateFilter', () => { const field = mockFieldDate; diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 1822f7eb3..e8fc93823 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -11,11 +11,14 @@ abstract class AbstractSearchFilter { corpusField: CorpusField; defaultData: FilterData; data: BehaviorSubject; + active: BehaviorSubject; constructor(corpusField: CorpusField) { this.corpusField = corpusField; this.defaultData = this.makeDefaultData(corpusField.filterOptions); this.data = new BehaviorSubject(this.defaultData); + this.active = new BehaviorSubject(false); + this.data.subscribe(this.deactivateWhenDefault.bind(this)); } get currentData() { @@ -28,15 +31,25 @@ abstract class AbstractSearchFilter { ); } + set(data: FilterData) { + if (!_.isEqual(data, this.currentData)) { + this.data.next(data); + + if (!_.isEqual(data, this.defaultData)) { + this.activate(); + } + } + } + reset() { - this.data.next(this.defaultData); + this.set(this.defaultData); } /** * set value based on route parameter */ setFromParam(param: string): void { - this.data.next(this.dataFromString(param)); + this.set(this.dataFromString(param)); } /** @@ -44,7 +57,7 @@ abstract class AbstractSearchFilter { * the same day, page, publication, etc. as a specific document) */ setToValue(value: any) { - this.data.next(this.dataFromValue(value)); + this.set(this.dataFromValue(value)); } toRouteParam(): {[param: string]: any} { @@ -53,7 +66,31 @@ abstract class AbstractSearchFilter { }; } - abstract makeDefaultData(filterOptions: FilterOptions): FilterData; + public activate() { + if (!this.active.value) { + this.toggle(); + } + } + + public deactivate() { + if (this.active.value) { + this.toggle(); + } + } + + public toggle() { + this.active.next(!this.active.value); + } + + + /** called after filter updates: deactivate the filter if the filter uses default data */ + private deactivateWhenDefault(isDefault: boolean) { + if (isDefault) { + this.deactivate(); + } + } + + abstract makeDefaultData(filterOptions: FilterOptions): FilterData; abstract dataFromValue(value: any): FilterData; @@ -67,6 +104,7 @@ abstract class AbstractSearchFilter { abstract toEsFilter(): EsFilterType; abstract dataFromEsFilter(esFilter: EsFilterType): FilterData; + } export interface DateFilterData { From 84a2fe76da8737c4d2b04c82de95fb73d95e3391 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 10:49:12 +0200 Subject: [PATCH 211/262] implement adHoc and description on SearchFilter --- frontend/src/app/models/search-filter.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index e8fc93823..bafa55c11 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -31,6 +31,20 @@ abstract class AbstractSearchFilter { ); } + + get adHoc() { + return _.isUndefined(this.corpusField.filterOptions); + } + + get description() { + if (this.corpusField?.filterOptions.description) { + return this.corpusField.filterOptions.description; + } else { + return `Filter results based on ${this.corpusField.displayName}`; + } + } + + set(data: FilterData) { if (!_.isEqual(data, this.currentData)) { this.data.next(data); From 18e0414c8c701169dcf43fa54c20b5821120d500 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 11:10:53 +0200 Subject: [PATCH 212/262] filter.toEsFilter only returns when active --- frontend/src/app/models/query.ts | 8 +++++-- frontend/src/app/models/search-filter.spec.ts | 19 +++++++++++---- frontend/src/app/models/search-filter.ts | 23 ++++++++++++------- frontend/src/app/utils/params.spec.ts | 2 +- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 1f9187654..c5567c315 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,7 +1,7 @@ import { ParamMap } from '@angular/router'; import * as _ from 'lodash'; import { combineLatest, Subject, Subscription } from 'rxjs'; -import { Corpus, CorpusField, SortBy, SortConfiguration, SortDirection, } from '../models/index'; +import { Corpus, CorpusField, EsFilter, SortBy, SortConfiguration, SortDirection, } from '../models/index'; import { EsQuery } from '../services'; import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; import { @@ -86,6 +86,10 @@ export class QueryModel { this.sort = new SortConfiguration(this.corpus); } + get activeFilters() { + return this.filters.filter(f => f.active.value); + } + setQueryText(text?: string) { this.queryText = text || undefined; this.update.next(); @@ -195,7 +199,7 @@ export class QueryModel { /** convert the query to an elasticsearch query */ toEsQuery(): EsQuery { - const filters = this.filters.map(filter => filter.toEsFilter()); + const filters = this.activeFilters.map(filter => filter.toEsFilter()) as EsFilter[]; const query = combineSearchClauseAndFilters(this.queryText, filters, this.searchFields); const sort = this.sort.toEsQuerySort(); diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index 0896f408c..2c00134d3 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -1,4 +1,5 @@ import { mockFieldMultipleChoice, mockFieldDate } from '../../mock-data/corpus'; +import { EsDateFilter, EsTermsFilter } from './elasticsearch'; import { DateFilter, DateFilterData, MultipleChoiceFilter } from './search-filter'; describe('SearchFilter', () => { @@ -56,6 +57,10 @@ describe('SearchFilter', () => { describe('DateFilter', () => { const field = mockFieldDate; let filter: DateFilter; + const exampleData = { + min: new Date(Date.parse('Jan 01 1850')), + max: new Date(Date.parse('Dec 31 1860')) + }; beforeEach(() => { filter = new DateFilter(field); @@ -85,12 +90,13 @@ describe('DateFilter', () => { }); it('should convert to an elasticsearch filter', () => { + filter.set(exampleData); const esFilter = filter.toEsFilter(); expect(esFilter).toEqual({ range: { date: { - gte: '1800-01-01', - lte: '1899-12-31', + gte: '1850-01-01', + lte: '1860-12-31', format: 'yyyy-MM-dd' } } @@ -98,6 +104,7 @@ describe('DateFilter', () => { }); it('should parse an elasticsearch filter', () => { + filter.set(exampleData); const esFilter = filter.toEsFilter(); expect(filter.dataFromEsFilter(esFilter)).toEqual(filter.currentData); }); @@ -106,6 +113,7 @@ describe('DateFilter', () => { describe('MultipleChoiceFilter', () => { const field = mockFieldMultipleChoice; let filter: MultipleChoiceFilter; + const exampleData = ['test']; beforeEach(() => { filter = new MultipleChoiceFilter(field); @@ -122,13 +130,13 @@ describe('MultipleChoiceFilter', () => { .toEqual(filter.currentData); // non-empty value - filter.data.next(['a', 'b', 'value with spaces']); + filter.set(['a', 'b', 'value with spaces']); expect(filter.dataFromString(filter.dataToString(filter.currentData))) .toEqual(filter.currentData); }); it('should convert values to valid URI components', () => { - filter.data.next(['a long value']); + filter.set(['a long value']); expect(filter.dataToString(filter.currentData)).not.toContain(' '); }); @@ -139,7 +147,7 @@ describe('MultipleChoiceFilter', () => { }); it('should convert to an elasticsearch filter', () => { - filter.data.next(['wow!', 'a great selection!']); + filter.set(['wow!', 'a great selection!']); const esFilter = filter.toEsFilter(); expect(esFilter).toEqual({ terms: { @@ -149,6 +157,7 @@ describe('MultipleChoiceFilter', () => { }); it('should parse an elasticsearch filter', () => { + filter.set(exampleData); const esFilter = filter.toEsFilter(); expect(filter.dataFromEsFilter(esFilter)).toEqual(filter.currentData); }); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index bafa55c11..fb531bf91 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -75,11 +75,18 @@ abstract class AbstractSearchFilter { } toRouteParam(): {[param: string]: any} { + const value = this.active.value ? this.dataToString(this.currentData) : undefined; return { - [this.corpusField.name]: this.dataToString(this.currentData) || null + [this.corpusField.name]: value || null }; } + toEsFilter(): EsFilterType { + if (this.active.value) { + return this.dataToEsFilter(); + } + } + public activate() { if (!this.active.value) { this.toggle(); @@ -113,9 +120,9 @@ abstract class AbstractSearchFilter { abstract dataToString(data: FilterData): string; /** - * export as filter specification in elasticsearch query language + * export data as filter specification in elasticsearch query language */ - abstract toEsFilter(): EsFilterType; + abstract dataToEsFilter(): EsFilterType; abstract dataFromEsFilter(esFilter: EsFilterType): FilterData; @@ -155,7 +162,7 @@ export class DateFilter extends AbstractSearchFilter { return data.toString(); } - toEsFilter(): EsTermFilter { + dataToEsFilter(): EsTermFilter { return { term: { [this.corpusField.name]: this.currentData diff --git a/frontend/src/app/utils/params.spec.ts b/frontend/src/app/utils/params.spec.ts index 3a72f93ae..6c2d13aaf 100644 --- a/frontend/src/app/utils/params.spec.ts +++ b/frontend/src/app/utils/params.spec.ts @@ -65,7 +65,7 @@ describe('paramsHaveChanged', () => { it('should detect new filters', () => { const filter = mockField.makeSearchFilter() as MultipleChoiceFilter; - filter.data.next(['test']); + filter.set(['test']); const params = convertToParamMap(filter.toRouteParam()); expect(paramsHaveChanged(queryModel, params)).toBeTrue(); From d0ebf7e79cdffaefd384b4812eb180d29508dfe2 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 12:29:48 +0200 Subject: [PATCH 213/262] setFromParams is only used in constructors this avoids complex update events where multiple properties must e updated simultaneously --- frontend/src/app/models/query.spec.ts | 49 ++------------------- frontend/src/app/models/query.ts | 26 +++++------ frontend/src/app/models/sort.spec.ts | 8 +--- frontend/src/app/models/sort.ts | 31 +++++++------ frontend/src/app/search/search.component.ts | 11 ++--- frontend/src/app/utils/params.spec.ts | 2 +- 6 files changed, 42 insertions(+), 85 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index abf254a3e..bec1aa70e 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -164,9 +164,9 @@ describe('QueryModel', () => { date: '1850-01-01:1850-01-01', }); - query.setFromParams(params); - expect(query.queryText).toEqual('test'); - expect(query.filters.length).toBe(1); + const newQuery = new QueryModel(corpus, params); + expect(newQuery.queryText).toEqual('test'); + expect(newQuery.filters.length).toBe(1); }); it('should formulate a link', () => { @@ -189,47 +189,4 @@ describe('QueryModel', () => { expect(query.filters[0].currentData.min).toEqual(new Date('Jan 2 1850')); expect(clone.filters[0].currentData.min).toEqual(new Date('Jan 1 1850')); }); - - it('should fire a single update event when updating from params', () => { - // dirty up the settings a bit - query.setQueryText('test'); - query.addFilter(filter); - query.sort.setSortBy(mockFieldDate); - query.sort.setSortDirection('desc'); - - let updates = 0; - query.update.subscribe(() => updates += 1); - - const params = convertToParamMap({}); - query.setFromParams(params); - expect(updates).toBe(1); - - const params2 = convertToParamMap({ - query: 'test', - ... filter.toRouteParam(), - ... filter2.toRouteParam(), - }); - - query.setFromParams(params2); - - expect(updates).toBe(2); - }); - - it('should not fire updates when params are unchanged', () => { - let updates = 0; - query.update.subscribe(() => updates += 1); - - const emptyParams = convertToParamMap({}); - - query.setFromParams(emptyParams); - - expect(updates).toBe(0); - - const params1 = convertToParamMap({ - query: 'test' - }); - query.setFromParams(params1); - - expect(updates).toBe(1); - }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index c5567c315..75ddb09ca 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -81,9 +81,12 @@ export class QueryModel { private filterSubscription: Subscription; - constructor(corpus: Corpus) { + constructor(corpus: Corpus, params?: ParamMap) { this.corpus = corpus; this.sort = new SortConfiguration(this.corpus); + if (params) { + this.setFromParams(params); + } } get activeFilters() { @@ -136,18 +139,6 @@ export class QueryModel { this.update.next(); } - /** set the query values from a parameter map */ - setFromParams(params: ParamMap) { - if (paramsHaveChanged(this, params)) { - this.queryText = queryFromParams(params); - this.searchFields = searchFieldsFromParams(params, this.corpus); - this.filters = filtersFromParams(params, this.corpus); - this.sort.setFromParams(params); - this.highlightSize = highlightFromParams(params); - this.subscribeToFilterUpdates(); - } - } - /** * make a clone of the current query. * optionally include querytext or a filter for the new query. @@ -210,6 +201,15 @@ export class QueryModel { }; } + /** set the query values from a parameter map */ + private setFromParams(params: ParamMap) { + this.queryText = queryFromParams(params); + this.searchFields = searchFieldsFromParams(params, this.corpus); + this.filters = filtersFromParams(params, this.corpus); + this.sort = new SortConfiguration(this.corpus, params); + this.highlightSize = highlightFromParams(params); + } + private subscribeToFilterUpdates() { if (this.filterSubscription) { this.filterSubscription.unsubscribe(); diff --git a/frontend/src/app/models/sort.spec.ts b/frontend/src/app/models/sort.spec.ts index a2476556a..9f3c13143 100644 --- a/frontend/src/app/models/sort.spec.ts +++ b/frontend/src/app/models/sort.spec.ts @@ -21,12 +21,8 @@ describe('SortConfiguration', () => { const param = sort.toRouteParam(); - // set the values to something else... - sort.setSortBy(undefined); - sort.setSortDirection('desc'); - - // now restore them from the parameter - sort.setFromParams(convertToParamMap(param)); + // now initialise from the parameter + sort = new SortConfiguration(mockCorpus3, convertToParamMap(param)); expect(sort.sortBy.value).toEqual(mockField3); expect(sort.sortDirection.value).toBe('asc'); diff --git a/frontend/src/app/models/sort.ts b/frontend/src/app/models/sort.ts index 916e12d60..c0362cee5 100644 --- a/frontend/src/app/models/sort.ts +++ b/frontend/src/app/models/sort.ts @@ -18,9 +18,12 @@ export class SortConfiguration { private defaultSortBy: SortBy; private defaultSortDirection: SortDirection = 'desc'; - constructor(private corpus: Corpus) { + constructor(private corpus: Corpus, params?: ParamMap) { this.defaultSortBy = this.corpus.fields.find(field => field.primarySort); this.sortBy.next(this.defaultSortBy); + if (params) { + this.setFromParams(params); + } } /** @@ -48,7 +51,19 @@ export class SortConfiguration { this.sortDirection.next(this.defaultSortDirection); } - setFromParams(params: ParamMap) { + toRouteParam(): {sort: string|null} { + if (this.isDefault) { + return {sort: null}; + } + return sortSettingsToParams(this.sortBy.value, this.sortDirection.value); + } + + /** convert this configuration to the 'sort' part of an elasticsearch query */ + toEsQuerySort(): { sort?: any } { + return makeSortSpecification(this.sortBy.value, this.sortDirection.value); + } + + private setFromParams(params: ParamMap) { if (params.has('sort')) { const [sortParam, ascParam] = params.get('sort').split(','); if ( sortParam === 'relevance' ) { @@ -62,16 +77,4 @@ export class SortConfiguration { this.reset(); } } - - toRouteParam(): {sort: string|null} { - if (this.isDefault) { - return {sort: null}; - } - return sortSettingsToParams(this.sortBy.value, this.sortDirection.value); - } - - /** convert this configuration to the 'sort' part of an elasticsearch query */ - toEsQuerySort(): { sort?: any } { - return makeSortSpecification(this.sortBy.value, this.sortDirection.value); - } } diff --git a/frontend/src/app/search/search.component.ts b/frontend/src/app/search/search.component.ts index ab671f732..c447c0cf4 100644 --- a/frontend/src/app/search/search.component.ts +++ b/frontend/src/app/search/search.component.ts @@ -7,6 +7,7 @@ import { CorpusService, DialogService, } from '../services/index'; import { ParamDirective } from '../param/param-directive'; import { AuthService } from '../services/auth.service'; import * as _ from 'lodash'; +import { paramsHaveChanged } from '../utils/params'; @Component({ selector: 'ia-search', @@ -74,7 +75,9 @@ export class SearchComponent extends ParamDirective { setStateFromParams(params: ParamMap) { this.tabIndex = params.has('visualize') ? 1 : 0; this.showVisualization = params.has('visualize') ? true : false; - this.queryModel.setFromParams(params); + if (paramsHaveChanged(this.queryModel, params)) { + this.setQueryModel(false); + } } @HostListener('window:scroll', []) @@ -128,10 +131,8 @@ export class SearchComponent extends ParamDirective { } private setQueryModel(reset: boolean) { - const queryModel = new QueryModel(this.corpus); - if (!reset) { - queryModel.setFromParams(this.route.snapshot.queryParamMap); - } + const params = reset ? undefined : this.route.snapshot.queryParamMap; + const queryModel = new QueryModel(this.corpus, params); this.queryModel = queryModel; this.queryText = queryModel.queryText; this.queryModel.update.subscribe(() => { diff --git a/frontend/src/app/utils/params.spec.ts b/frontend/src/app/utils/params.spec.ts index 6c2d13aaf..d5b519622 100644 --- a/frontend/src/app/utils/params.spec.ts +++ b/frontend/src/app/utils/params.spec.ts @@ -56,7 +56,7 @@ describe('paramsHaveChanged', () => { expect(paramsHaveChanged(queryModel, params1)).toBeFalse(); expect(paramsHaveChanged(queryModel, params2)).toBeTrue(); - queryModel.setFromParams(params2); + queryModel = new QueryModel(corpus, params2); expect(paramsHaveChanged(queryModel, params2)).toBeFalse(); expect(paramsHaveChanged(queryModel, params1)).toBeTrue(); From 671019567fe6591f804d5f10c46f4a24aaee4ec3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 12:32:32 +0200 Subject: [PATCH 214/262] filter.data.next -> filter.set, query.filters -> query.activeFilters --- .../app/filter/date-filter.component.spec.ts | 2 +- .../filter/filter-manager.component.spec.ts | 4 ++-- .../app/filter/range-filter.component.spec.ts | 2 +- .../search-history/query-filters.component.ts | 2 +- .../src/app/models/filter-management.spec.ts | 18 +++++++++--------- frontend/src/app/models/filter-management.ts | 2 +- frontend/src/app/models/query.spec.ts | 6 +++--- frontend/src/app/utils/params.ts | 2 +- .../barchart/timeline.component.ts | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/filter/date-filter.component.spec.ts b/frontend/src/app/filter/date-filter.component.spec.ts index b0364368e..79750cf4c 100644 --- a/frontend/src/app/filter/date-filter.component.spec.ts +++ b/frontend/src/app/filter/date-filter.component.spec.ts @@ -19,7 +19,7 @@ describe('DateFilterComponent', () => { component = fixture.componentInstance; const queryModel = new QueryModel(mockCorpus3); component.filter = new PotentialFilter(mockFieldDate, queryModel); - component.filter.filter.data.next({ + component.filter.filter.set({ min: new Date('Jan 1 1810'), max: new Date('Dec 31 1820') }); diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index 4443e20c5..710982fb9 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -44,8 +44,8 @@ describe('FilterManagerComponent', () => { it('toggles filters on and off', async () => { const filter = component.potentialFilters.find(f => f.corpusField.name === 'great_field'); - expect(component.queryModel.filters.length).toBe(0); + expect(component.queryModel.activeFilters.length).toBe(0); filter.toggle(); - expect(component.queryModel.filters.length).toBe(1); + expect(component.queryModel.activeFilters.length).toBe(1); }); }); diff --git a/frontend/src/app/filter/range-filter.component.spec.ts b/frontend/src/app/filter/range-filter.component.spec.ts index e82968366..bfb1340bc 100644 --- a/frontend/src/app/filter/range-filter.component.spec.ts +++ b/frontend/src/app/filter/range-filter.component.spec.ts @@ -19,7 +19,7 @@ describe('RangeFilterComponent', () => { component = fixture.componentInstance; const query = new QueryModel(mockCorpus3); component.filter = new PotentialFilter(mockField3, query); - component.filter.filter.data.next({min: 1984, max: 1984}); + component.filter.filter.set({min: 1984, max: 1984}); fixture.detectChanges(); }); diff --git a/frontend/src/app/history/search-history/query-filters.component.ts b/frontend/src/app/history/search-history/query-filters.component.ts index a3e5406e5..b382ada5c 100644 --- a/frontend/src/app/history/search-history/query-filters.component.ts +++ b/frontend/src/app/history/search-history/query-filters.component.ts @@ -17,7 +17,7 @@ export class QueryFiltersComponent implements OnInit { ngOnInit() { if (this.queryModel.filters?.length>0) { - this.formattedFilters = this.queryModel.filters.map(filter => ({ + this.formattedFilters = this.queryModel.activeFilters.map(filter => ({ name: filter.corpusField.name, formattedData: filter.dataToString(filter.currentData) })); diff --git a/frontend/src/app/models/filter-management.spec.ts b/frontend/src/app/models/filter-management.spec.ts index 9189dca22..b9bfc4e78 100644 --- a/frontend/src/app/models/filter-management.spec.ts +++ b/frontend/src/app/models/filter-management.spec.ts @@ -14,13 +14,13 @@ describe('PotentialFilter', () => { const field = mockField; const query = new QueryModel(mockCorpus); const potentialFilter = new PotentialFilter(field, query); - potentialFilter.filter.data.next(true); + potentialFilter.filter.set(true); - expect(query.filters.length).toBe(0); + expect(query.activeFilters.length).toBe(0); potentialFilter.toggle(); - expect(query.filters.length).toBe(1); + expect(query.activeFilters.length).toBe(1); potentialFilter.toggle(); - expect(query.filters.length).toBe(0); + expect(query.activeFilters.length).toBe(0); }); it('should deactivate when a date filter resets', () => { @@ -30,10 +30,10 @@ describe('PotentialFilter', () => { potentialFilter.filter.setToValue('Jan 1 1850'); potentialFilter.activate(); - expect(query.filters.length).toBe(1); + expect(query.activeFilters.length).toBe(1); potentialFilter.filter.reset(); - expect(query.filters.length).toBe(0); + expect(query.activeFilters.length).toBe(0); }); it('should deactivate when a multiple choice filter resets', () => { @@ -43,9 +43,9 @@ describe('PotentialFilter', () => { potentialFilter.filter.setToValue('test'); potentialFilter.activate(); - expect(query.filters.length).toBe(1); + expect(query.activeFilters.length).toBe(1); - potentialFilter.filter.data.next([]); - expect(query.filters.length).toBe(0); + potentialFilter.filter.set([]); + expect(query.activeFilters.length).toBe(0); }); }); diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index a21ceffb3..ad20f1c16 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -57,7 +57,7 @@ export class PotentialFilter { set(data: any) { if (!_.isEqual(data, this.filter.currentData)) { - this.filter.data.next(data); + this.filter.set(data); if (!_.isEqual(data, this.filter.defaultData)) { this.activate(); diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index bec1aa70e..6b8ab6613 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -66,7 +66,7 @@ describe('QueryModel', () => { query.addFilter(filter); query.addFilter(filter2); - expect(query.filters.length).toBe(2); + expect(query.activeFilters.length).toBe(2); expect(updates).toBe(2); filter.setToValue(new Date('Jan 1 1860')); @@ -75,7 +75,7 @@ describe('QueryModel', () => { query.removeFilter(filter); - expect(query.filters.length).toBe(1); + expect(query.activeFilters.length).toBe(1); expect(updates).toBe(4); filter.setToValue(new Date('Jan 1 1870')); @@ -166,7 +166,7 @@ describe('QueryModel', () => { const newQuery = new QueryModel(corpus, params); expect(newQuery.queryText).toEqual('test'); - expect(newQuery.filters.length).toBe(1); + expect(newQuery.activeFilters.length).toBe(1); }); it('should formulate a link', () => { diff --git a/frontend/src/app/utils/params.ts b/frontend/src/app/utils/params.ts index a2dd03a32..5e2491ecf 100644 --- a/frontend/src/app/utils/params.ts +++ b/frontend/src/app/utils/params.ts @@ -40,7 +40,7 @@ export const filtersFromParams = (params: ParamMap, corpus: Corpus): SearchFilte return specifiedFields.map(field => { const filter = field.makeSearchFilter(); const data = filter.dataFromString(params.get(field.name)); - filter.data.next(data); + filter.set(data); return filter; }); }; diff --git a/frontend/src/app/visualization/barchart/timeline.component.ts b/frontend/src/app/visualization/barchart/timeline.component.ts index c7ed20611..60292d2e1 100644 --- a/frontend/src/app/visualization/barchart/timeline.component.ts +++ b/frontend/src/app/visualization/barchart/timeline.component.ts @@ -272,7 +272,7 @@ export class TimelineComponent extends BarchartDirective impl const queryModelCopy = query.clone(); // download zoomed in results const filter = this.visualizedField.makeSearchFilter(); - filter.data.next({ min, max }); + filter.set({ min, max }); queryModelCopy.addFilter(filter); return queryModelCopy; } From c88ec3d0e494fdc31048869fe2627cde6bfd43d6 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 12:48:16 +0200 Subject: [PATCH 215/262] simplify deactivation logic --- .../filter/filter-manager.component.spec.ts | 6 ++++- frontend/src/app/models/search-filter.ts | 27 +++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index 710982fb9..fa0bad08e 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -45,7 +45,11 @@ describe('FilterManagerComponent', () => { it('toggles filters on and off', async () => { const filter = component.potentialFilters.find(f => f.corpusField.name === 'great_field'); expect(component.queryModel.activeFilters.length).toBe(0); - filter.toggle(); + filter.set(['test']); + expect(component.queryModel.activeFilters.length).toBe(1); + filter.filter.toggle(); + expect(component.queryModel.activeFilters.length).toBe(0); + filter.filter.toggle(); expect(component.queryModel.activeFilters.length).toBe(1); }); }); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index fb531bf91..108b73731 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import * as moment from 'moment'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; +import { distinct, map } from 'rxjs/operators'; import { CorpusField } from './corpus'; import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, EsTermFilter } from './elasticsearch'; import { BooleanFilterOptions, DateFilterOptions, FilterOptions, MultipleChoiceFilterOptions, @@ -18,7 +18,6 @@ abstract class AbstractSearchFilter { this.defaultData = this.makeDefaultData(corpusField.filterOptions); this.data = new BehaviorSubject(this.defaultData); this.active = new BehaviorSubject(false); - this.data.subscribe(this.deactivateWhenDefault.bind(this)); } get currentData() { @@ -44,6 +43,18 @@ abstract class AbstractSearchFilter { } } + /** + * an observable of the effective predicate imposed by the filter. + * + * emits the latest filter data wile the filter is active, and undefined + * when it is set to inactive. + */ + get activeData$(): Observable { + return combineLatest([this.active, this.data]).pipe( + map(values => values[0] ? values[1] : undefined), + distinct(), + ); + } set(data: FilterData) { if (!_.isEqual(data, this.currentData)) { @@ -51,6 +62,8 @@ abstract class AbstractSearchFilter { if (!_.isEqual(data, this.defaultData)) { this.activate(); + } else { + this.deactivate(); } } } @@ -103,14 +116,6 @@ abstract class AbstractSearchFilter { this.active.next(!this.active.value); } - - /** called after filter updates: deactivate the filter if the filter uses default data */ - private deactivateWhenDefault(isDefault: boolean) { - if (isDefault) { - this.deactivate(); - } - } - abstract makeDefaultData(filterOptions: FilterOptions): FilterData; abstract dataFromValue(value: any): FilterData; From 8aa749841a2e280a9df2f2b1b9ddd507d60e484e Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 13:21:56 +0200 Subject: [PATCH 216/262] remove duplicate functionality --- .../src/app/models/filter-management.spec.ts | 4 +- frontend/src/app/models/filter-management.ts | 40 +++++-------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/models/filter-management.spec.ts b/frontend/src/app/models/filter-management.spec.ts index b9bfc4e78..5cfdb09fd 100644 --- a/frontend/src/app/models/filter-management.spec.ts +++ b/frontend/src/app/models/filter-management.spec.ts @@ -16,11 +16,11 @@ describe('PotentialFilter', () => { const potentialFilter = new PotentialFilter(field, query); potentialFilter.filter.set(true); - expect(query.activeFilters.length).toBe(0); - potentialFilter.toggle(); expect(query.activeFilters.length).toBe(1); potentialFilter.toggle(); expect(query.activeFilters.length).toBe(0); + potentialFilter.toggle(); + expect(query.activeFilters.length).toBe(1); }); it('should deactivate when a date filter resets', () => { diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts index ad20f1c16..ad9edeaed 100644 --- a/frontend/src/app/models/filter-management.ts +++ b/frontend/src/app/models/filter-management.ts @@ -1,24 +1,26 @@ import * as _ from 'lodash'; -import { BehaviorSubject } from 'rxjs'; import { CorpusField } from './corpus'; import { QueryModel } from './query'; import { SearchFilter } from './search-filter'; import { SearchFilterType } from './search-filter-options'; +import { Observable } from 'rxjs-compat'; export class PotentialFilter { filter: SearchFilter; - useAsFilter = new BehaviorSubject(false); + useAsFilter: Observable; description: string; adHoc?: boolean; constructor(public corpusField: CorpusField, public queryModel: QueryModel) { if (queryModel.filterForField(corpusField)) { this.filter = queryModel.filterForField(corpusField); - this.useAsFilter.next(true); } else { this.filter = corpusField.makeSearchFilter(); + this.queryModel.addFilter(this.filter); } + this.useAsFilter = this.filter.active.asObservable(); + if (!corpusField.filterOptions) { this.description = `View results from this ${corpusField.displayName}`; this.adHoc = true; @@ -26,8 +28,6 @@ export class PotentialFilter { this.description = corpusField.filterOptions.description; this.adHoc = false; } - - this.filter.isDefault$.subscribe(this.deactivateWhenDefault.bind(this)); } get filterType(): SearchFilterType { @@ -35,44 +35,22 @@ export class PotentialFilter { } toggle() { - this.useAsFilter.next(!this.useAsFilter.value); - if (this.useAsFilter.value) { - this.queryModel.addFilter(this.filter); - } else { - this.queryModel.removeFilter(this.filter); - } + this.filter.toggle(); } deactivate() { - if (this.useAsFilter.value) { - this.toggle(); - } + this.filter.deactivate(); } activate() { - if (!this.useAsFilter.value) { - this.toggle(); - } + this.filter.activate(); } set(data: any) { - if (!_.isEqual(data, this.filter.currentData)) { - this.filter.set(data); - - if (!_.isEqual(data, this.filter.defaultData)) { - this.activate(); - } - } + this.filter.set(data); } reset() { this.filter.reset(); } - - /** called after filter updates: deactivate the filter if the filter uses default data */ - private deactivateWhenDefault(isDefault: boolean) { - if (isDefault) { - this.deactivate(); - } - } } From e756ec02ff9d81e26565d4012a0ddde5f3e4b134 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 13:30:24 +0200 Subject: [PATCH 217/262] addFilter / removeFilter change activation values --- frontend/src/app/models/query.spec.ts | 3 +-- frontend/src/app/models/query.ts | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 6b8ab6613..c89a0d5fa 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -80,8 +80,7 @@ describe('QueryModel', () => { filter.setToValue(new Date('Jan 1 1870')); - expect(updates).toBe(4); - + expect(updates).toBe(5); }); it('should convert to an elasticsearch query', () => { diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 75ddb09ca..36e32f97d 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -87,6 +87,7 @@ export class QueryModel { if (params) { this.setFromParams(params); } + this.subscribeToFilterUpdates(); } get activeFilters() { @@ -99,8 +100,12 @@ export class QueryModel { } addFilter(filter: SearchFilter) { - this.filters.push(filter); - this.subscribeToFilterUpdates(); + if (this.filterForField(filter.corpusField)) { + this.filterForField(filter.corpusField).set(filter.data); + } else { + this.filters.push(filter); + this.subscribeToFilterUpdates(); + } } @@ -116,7 +121,7 @@ export class QueryModel { removeFilter(filter: SearchFilter) { - this.removeFiltersForField(filter.corpusField); + this.deactivateFiltersForField(filter.corpusField); } /** get an active search filter on this query for the field (undefined if none exists) */ @@ -125,13 +130,12 @@ export class QueryModel { } /** remove all filters that apply to a corpus field */ - removeFiltersForField(field: CorpusField) { - if (this.filterForField(field)) { - _.remove(this.filters, - filter => filter.corpusField.name === field.name - ); - this.subscribeToFilterUpdates(); - } + deactivateFiltersForField(field: CorpusField) { + this.filters.filter(filter => + filter.corpusField.name === field.name + ).forEach(filter => + filter.deactivate() + ); } setHighlight(size?: number) { @@ -216,7 +220,7 @@ export class QueryModel { } if (this.filters.length) { this.filterSubscription = combineLatest( - this.filters.map(f => f.data) + this.filters.map(f => f.activeData$) ).subscribe(() => this.update.next()); } else { this.filterSubscription = undefined; From 8860765cfdde3179051aaba9cfaf26e52a707366 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 13:34:47 +0200 Subject: [PATCH 218/262] filters can set from parammap --- frontend/src/app/models/search-filter.spec.ts | 15 +++++++++++++++ frontend/src/app/models/search-filter.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index 2c00134d3..07b8895d6 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -1,3 +1,4 @@ +import { convertToParamMap } from '@angular/router'; import { mockFieldMultipleChoice, mockFieldDate } from '../../mock-data/corpus'; import { EsDateFilter, EsTermsFilter } from './elasticsearch'; import { DateFilter, DateFilterData, MultipleChoiceFilter } from './search-filter'; @@ -52,6 +53,20 @@ describe('SearchFilter', () => { filter.reset(); expect(isActive()).toBeFalse(); }); + + it('should set from parameters', () => { + filter.setFromParams(convertToParamMap({ + date: '1850-01-01:1860-01-01' + })); + + expect(filter.active.value).toBeTrue(); + + filter.setFromParams(convertToParamMap({ + query: 'test' + })); + + expect(filter.active.value).toBeFalse(); + }); }); describe('DateFilter', () => { diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 108b73731..02eedc53f 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -6,6 +6,7 @@ import { CorpusField } from './corpus'; import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, EsTermFilter } from './elasticsearch'; import { BooleanFilterOptions, DateFilterOptions, FilterOptions, MultipleChoiceFilterOptions, RangeFilterOptions } from './search-filter-options'; +import { ParamMap } from '@angular/router'; abstract class AbstractSearchFilter { corpusField: CorpusField; @@ -75,8 +76,13 @@ abstract class AbstractSearchFilter { /** * set value based on route parameter */ - setFromParam(param: string): void { - this.set(this.dataFromString(param)); + setFromParams(params: ParamMap): void { + const value = params.get(this.corpusField.name); + if (value) { + this.set(this.dataFromString(value)); + } else { + this.reset(); + } } /** From 8120b0d5d05756003d8afb4b74d1950c79007895 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 9 Jun 2023 14:14:15 +0200 Subject: [PATCH 219/262] prevent errors from using filters for non-existent fields --- frontend/src/app/filter/filter-manager.component.ts | 9 +-------- .../app/filter/multiple-choice-filter.component.spec.ts | 5 ++++- frontend/src/mock-data/corpus.ts | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 72d8b06e2..7d1f7f346 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -13,14 +13,7 @@ import { PotentialFilter, Corpus, SearchFilter, QueryModel } from '../models/ind styleUrls: ['./filter-manager.component.scss'] }) export class FilterManagerComponent { - @Input() - get corpus(): Corpus { - return this._corpus; - } - set corpus(corpus: Corpus) { - this._corpus = corpus; - this.setPotentialFilters(); - } + @Input() corpus: Corpus; @Input() get queryModel(): QueryModel { diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index 1220467e1..5e00b154d 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -5,6 +5,7 @@ import { commonTestBed } from '../common-test-bed'; import { PotentialFilter, QueryModel } from '../models'; import { MultipleChoiceFilterComponent } from './multiple-choice-filter.component'; +import * as _ from 'lodash'; describe('MultipleChoiceFilterComponent', () => { let component: MultipleChoiceFilterComponent; @@ -17,7 +18,9 @@ describe('MultipleChoiceFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MultipleChoiceFilterComponent); component = fixture.componentInstance; - const query = new QueryModel(mockCorpus); + const corpus = _.cloneDeep(mockCorpus); + corpus.fields.push(mockFieldMultipleChoice); + const query = new QueryModel(corpus); component.filter = new PotentialFilter(mockFieldMultipleChoice, query); fixture.detectChanges(); }); diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index f7330c0fa..2ed93c628 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -177,7 +177,7 @@ export const mockCorpus3: Corpus = { scan_image_type: 'pdf', allow_image_download: false, word_models_present: false, - fields: [mockField, mockField2, mockField3, mockFieldDate], + fields: [mockField, mockField2, mockField3, mockFieldDate, mockFieldMultipleChoice], documentContext: { contextFields: [mockFieldDate], displayName: 'day', From 033c60884ff64e3e8a297b3eebfdf4d9b61a27f4 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 15:39:08 +0200 Subject: [PATCH 220/262] initiate all filters in query constructor --- frontend/src/app/models/query.spec.ts | 37 +++++++------ frontend/src/app/models/query.ts | 52 ++++++------------- frontend/src/app/models/search-filter.spec.ts | 17 ++++++ frontend/src/app/models/search-filter.ts | 31 +++++------ 4 files changed, 63 insertions(+), 74 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index c89a0d5fa..f02ee5a75 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -28,16 +28,14 @@ describe('QueryModel', () => { let filter: SearchFilter; let filter2: SearchFilter; - beforeEach(() => { - query = new QueryModel(corpus); - }); + const someDate = new Date('Jan 1 1850'); + const someSelection = ['hooray!']; beforeEach(() => { - filter = new DateFilter(mockFieldDate); - filter.setToValue(new Date('Jan 1 1850')); + query = new QueryModel(corpus); - filter2 = new MultipleChoiceFilter(mockFieldMultipleChoice); - filter2.setToValue(['hooray!']); + filter = query.filterForField(mockFieldDate); + filter2 = query.filterForField(mockFieldMultipleChoice); }); it('should create', () => { @@ -51,35 +49,36 @@ describe('QueryModel', () => { query.setQueryText('test'); expect(updates).toBe(1); - query.addFilter(filter); + filter.setToValue(someDate); expect(updates).toBe(2); - query.removeFilter(filter); + filter.deactivate(); expect(updates).toBe(3); - }); it('should remove filters', () => { let updates = 0; query.update.subscribe(() => updates += 1); - query.addFilter(filter); - query.addFilter(filter2); + filter.setToValue(someDate); + filter2.setToValue(someSelection); expect(query.activeFilters.length).toBe(2); expect(updates).toBe(2); filter.setToValue(new Date('Jan 1 1860')); + expect(query.activeFilters.length).toBe(2); expect(updates).toBe(3); - query.removeFilter(filter); + filter.deactivate(); expect(query.activeFilters.length).toBe(1); expect(updates).toBe(4); filter.setToValue(new Date('Jan 1 1870')); + expect(query.activeFilters.length).toBe(2); expect(updates).toBe(5); }); @@ -131,7 +130,7 @@ describe('QueryModel', () => { highlight: null, }); - query.addFilter(filter); + filter.setToValue(someDate); expect(query.toRouteParam()).toEqual({ query: 'test', @@ -144,7 +143,7 @@ describe('QueryModel', () => { }); query.setQueryText(''); - query.removeFilter(filter); + filter.deactivate(); expect(query.toRouteParam()).toEqual({ query: null, @@ -170,14 +169,14 @@ describe('QueryModel', () => { it('should formulate a link', () => { query.setQueryText('test'); - query.addFilter(filter); + filter.setToValue(someDate); expect(query.toQueryParams()).toEqual({ query: 'test', date: '1850-01-01:1850-01-01' }); }); it('should clone', () => { query.setQueryText('test'); - query.addFilter(filter); + filter.setToValue(someDate); const clone = query.clone(); @@ -185,7 +184,7 @@ describe('QueryModel', () => { expect(clone.queryText).toEqual('test'); filter.setToValue(new Date('Jan 2 1850')); - expect(query.filters[0].currentData.min).toEqual(new Date('Jan 2 1850')); - expect(clone.filters[0].currentData.min).toEqual(new Date('Jan 1 1850')); + expect(query.filterForField(mockFieldDate).currentData.min).toEqual(new Date('Jan 2 1850')); + expect(clone.filterForField(mockFieldDate).currentData.min).toEqual(new Date('Jan 1 1850')); }); }); diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 36e32f97d..361ad1bfe 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,11 +1,11 @@ -import { ParamMap } from '@angular/router'; +import { convertToParamMap, ParamMap } from '@angular/router'; import * as _ from 'lodash'; -import { combineLatest, Subject, Subscription } from 'rxjs'; +import { combineLatest, Subject } from 'rxjs'; import { Corpus, CorpusField, EsFilter, SortBy, SortConfiguration, SortDirection, } from '../models/index'; import { EsQuery } from '../services'; import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; import { - filtersFromParams, highlightFromParams, omitNullParameters, paramsHaveChanged, queryFiltersToParams, + filtersFromParams, highlightFromParams, omitNullParameters, queryFiltersToParams, queryFromParams, searchFieldsFromParams } from '../utils/params'; import { SearchFilter } from './search-filter'; @@ -69,20 +69,20 @@ export interface SearchParameters { size: number; } + export class QueryModel { corpus: Corpus; queryText: string; searchFields: CorpusField[]; - filters: SearchFilter[] = []; + filters: SearchFilter[]; sort: SortConfiguration; highlightSize: number; update = new Subject(); - private filterSubscription: Subscription; - constructor(corpus: Corpus, params?: ParamMap) { this.corpus = corpus; + this.filters = this.corpus.fields.map(field => field.makeSearchFilter()); this.sort = new SortConfiguration(this.corpus); if (params) { this.setFromParams(params); @@ -100,12 +100,7 @@ export class QueryModel { } addFilter(filter: SearchFilter) { - if (this.filterForField(filter.corpusField)) { - this.filterForField(filter.corpusField).set(filter.data); - } else { - this.filters.push(filter); - this.subscribeToFilterUpdates(); - } + this.filterForField(filter.corpusField).set(filter.currentData); } @@ -145,19 +140,9 @@ export class QueryModel { /** * make a clone of the current query. - * optionally include querytext or a filter for the new query. */ - clone(queryText: string = undefined, addFilter: SearchFilter = undefined) { - const newQuery = _.clone(this); // or cloneDeep? - if (queryText !== undefined) { - newQuery.setQueryText(queryText); - } - if (addFilter) { - newQuery.addFilter(addFilter); - } - // deep clone filters so they are disconnected from the current query - newQuery.filters = _.cloneDeep(newQuery.filters); - return newQuery; + clone() { + return new QueryModel(this.corpus, convertToParamMap(this.toQueryParams())); } /** @@ -209,23 +194,16 @@ export class QueryModel { private setFromParams(params: ParamMap) { this.queryText = queryFromParams(params); this.searchFields = searchFieldsFromParams(params, this.corpus); - this.filters = filtersFromParams(params, this.corpus); + filtersFromParams(params, this.corpus).forEach(filter => { + this.filterForField(filter.corpusField).set(filter.data.value); + }); this.sort = new SortConfiguration(this.corpus, params); this.highlightSize = highlightFromParams(params); } private subscribeToFilterUpdates() { - if (this.filterSubscription) { - this.filterSubscription.unsubscribe(); - } - if (this.filters.length) { - this.filterSubscription = combineLatest( - this.filters.map(f => f.activeData$) - ).subscribe(() => this.update.next()); - } else { - this.filterSubscription = undefined; - this.update.next(); - } + this.filters.forEach(filter => { + filter.update.subscribe(() => this.update.next()); + }); } - } diff --git a/frontend/src/app/models/search-filter.spec.ts b/frontend/src/app/models/search-filter.spec.ts index 07b8895d6..714cc3172 100644 --- a/frontend/src/app/models/search-filter.spec.ts +++ b/frontend/src/app/models/search-filter.spec.ts @@ -2,6 +2,8 @@ import { convertToParamMap } from '@angular/router'; import { mockFieldMultipleChoice, mockFieldDate } from '../../mock-data/corpus'; import { EsDateFilter, EsTermsFilter } from './elasticsearch'; import { DateFilter, DateFilterData, MultipleChoiceFilter } from './search-filter'; +import { of } from 'rxjs'; +import { distinct } from 'rxjs/operators'; describe('SearchFilter', () => { // while these tests are ran on the DateFilter, @@ -67,6 +69,21 @@ describe('SearchFilter', () => { expect(filter.active.value).toBeFalse(); }); + + it('should signal updates', () => { + let updates = 0; + filter.update.subscribe(() => updates += 1); + + filter.set(exampleData); + expect(updates).toBe(1); + + filter.deactivate(); + expect(updates).toBe(2); + + filter.reset(); // this does not affect anything since the filter is inactive + expect(updates).toBe(2); + + }); }); describe('DateFilter', () => { diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index 02eedc53f..e9997f4e5 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import * as moment from 'moment'; -import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; +import { BehaviorSubject, Observable, Subject, combineLatest } from 'rxjs'; import { distinct, map } from 'rxjs/operators'; import { CorpusField } from './corpus'; import { EsBooleanFilter, EsDateFilter, EsFilter, EsTermsFilter, EsRangeFilter, EsTermFilter } from './elasticsearch'; @@ -14,6 +14,8 @@ abstract class AbstractSearchFilter { data: BehaviorSubject; active: BehaviorSubject; + update = new Subject(); + constructor(corpusField: CorpusField) { this.corpusField = corpusField; this.defaultData = this.makeDefaultData(corpusField.filterOptions); @@ -44,27 +46,19 @@ abstract class AbstractSearchFilter { } } - /** - * an observable of the effective predicate imposed by the filter. - * - * emits the latest filter data wile the filter is active, and undefined - * when it is set to inactive. - */ - get activeData$(): Observable { - return combineLatest([this.active, this.data]).pipe( - map(values => values[0] ? values[1] : undefined), - distinct(), - ); - } - set(data: FilterData) { if (!_.isEqual(data, this.currentData)) { this.data.next(data); - if (!_.isEqual(data, this.defaultData)) { - this.activate(); - } else { - this.deactivate(); + const active = this.active.value; + const toDefault = _.isEqual(data, this.defaultData); + const deactivate = active && toDefault; + const activate = !active && !toDefault; + + if (deactivate || activate) { + this.toggle(); + } else if (active) { + this.update.next(); } } } @@ -120,6 +114,7 @@ abstract class AbstractSearchFilter { public toggle() { this.active.next(!this.active.value); + this.update.next(); } abstract makeDefaultData(filterOptions: FilterOptions): FilterData; From b9d2dd61dcf339bb520a4c22b05ee6fa580ee0ae Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 16:25:48 +0200 Subject: [PATCH 221/262] filter manager component uses query model filters --- .../src/app/filter/base-filter.component.ts | 12 ++--- .../app/filter/boolean-filter.component.html | 2 +- .../filter/boolean-filter.component.spec.ts | 6 +-- .../app/filter/date-filter.component.spec.ts | 6 +-- .../app/filter/filter-manager.component.html | 22 +++++----- .../filter/filter-manager.component.spec.ts | 16 +++---- .../app/filter/filter-manager.component.ts | 44 +++++++------------ .../multiple-choice-filter.component.spec.ts | 4 +- .../multiple-choice-filter.component.ts | 4 +- .../app/filter/range-filter.component.spec.ts | 6 +-- frontend/src/app/models/search-filter.ts | 8 +++- 11 files changed, 61 insertions(+), 69 deletions(-) diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 41a8834ca..451ec88d1 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import * as _ from 'lodash'; -import { PotentialFilter } from '../models/index'; +import { QueryModel, SearchFilter } from '../models/index'; /** * Filter component receives the corpus fields containing search filters as input @@ -12,23 +12,25 @@ import { PotentialFilter } from '../models/index'; template: '' }) export abstract class BaseFilterComponent { - private _filter: PotentialFilter; + private _filter: SearchFilter; constructor() { } get data(): FilterData { - return this.filter?.filter.currentData; + return this.filter?.currentData; } @Input() get filter() { return this._filter; } - set filter(filter: PotentialFilter) { + set filter(filter: SearchFilter) { this._filter = filter; - this.onFilterSet(filter.filter); + this.onFilterSet(filter); } + @Input() queryModel: QueryModel; + /** * Trigger a change event. diff --git a/frontend/src/app/filter/boolean-filter.component.html b/frontend/src/app/filter/boolean-filter.component.html index 7e6a49c60..7e4c0a6b7 100644 --- a/frontend/src/app/filter/boolean-filter.component.html +++ b/frontend/src/app/filter/boolean-filter.component.html @@ -1,4 +1,4 @@

- + {{data | json | titlecase }}
diff --git a/frontend/src/app/filter/boolean-filter.component.spec.ts b/frontend/src/app/filter/boolean-filter.component.spec.ts index 2d1909ff9..95ab41254 100644 --- a/frontend/src/app/filter/boolean-filter.component.spec.ts +++ b/frontend/src/app/filter/boolean-filter.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { mockCorpus3, mockField } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; -import { PotentialFilter, QueryModel } from '../models'; +import { QueryModel } from '../models'; import { BooleanFilterComponent } from './boolean-filter.component'; @@ -17,8 +17,8 @@ describe('BooleanFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(BooleanFilterComponent); component = fixture.componentInstance; - const query = new QueryModel(mockCorpus3); - component.filter = new PotentialFilter(mockField, query); + component.queryModel = new QueryModel(mockCorpus3); + component.filter = component.queryModel.filterForField(mockField); fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/date-filter.component.spec.ts b/frontend/src/app/filter/date-filter.component.spec.ts index 79750cf4c..90b202d1c 100644 --- a/frontend/src/app/filter/date-filter.component.spec.ts +++ b/frontend/src/app/filter/date-filter.component.spec.ts @@ -17,9 +17,9 @@ describe('DateFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DateFilterComponent); component = fixture.componentInstance; - const queryModel = new QueryModel(mockCorpus3); - component.filter = new PotentialFilter(mockFieldDate, queryModel); - component.filter.filter.set({ + component.queryModel = new QueryModel(mockCorpus3); + component.filter = component.queryModel.filterForField(mockFieldDate); + component.filter.set({ min: new Date('Jan 1 1810'), max: new Date('Dec 31 1820') }); diff --git a/frontend/src/app/filter/filter-manager.component.html b/frontend/src/app/filter/filter-manager.component.html index ae58c2f9e..e03cafafe 100644 --- a/frontend/src/app/filter/filter-manager.component.html +++ b/frontend/src/app/filter/filter-manager.component.html @@ -17,8 +17,8 @@

Filters

- -
+ +
@@ -26,28 +26,28 @@

Filters

}}

- -

- + - - - - + + + +
diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index fa0bad08e..c152787db 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -24,32 +24,32 @@ describe('FilterManagerComponent', () => { it('should create', () => { expect(component).toBeTruthy(); - expect(component.potentialFilters.length).toEqual(2); + expect(component.filters.length).toEqual(2); }); it('resets filters when corpus changes', () => { component.corpus = mockCorpus2; component.queryModel = new QueryModel(mockCorpus2); fixture.detectChanges(); - expect(component.potentialFilters.length).toEqual(1); - expect(component.potentialFilters[0].adHoc).toBeTrue(); + expect(component.filters.length).toEqual(1); + expect(component.filters[0].adHoc).toBeTrue(); component.corpus = mockCorpus; component.queryModel = new QueryModel(mockCorpus); fixture.detectChanges(); - expect(component.potentialFilters.length).toEqual(2); - expect(component.potentialFilters[0].adHoc).toBeFalse(); + expect(component.filters.length).toEqual(2); + expect(component.filters[0].adHoc).toBeFalse(); }); it('toggles filters on and off', async () => { - const filter = component.potentialFilters.find(f => f.corpusField.name === 'great_field'); + const filter = component.filters.find(f => f.corpusField.name === 'great_field'); expect(component.queryModel.activeFilters.length).toBe(0); filter.set(['test']); expect(component.queryModel.activeFilters.length).toBe(1); - filter.filter.toggle(); + filter.toggle(); expect(component.queryModel.activeFilters.length).toBe(0); - filter.filter.toggle(); + filter.toggle(); expect(component.queryModel.activeFilters.length).toBe(1); }); }); diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index 7d1f7f346..ccb6232e9 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -5,7 +5,7 @@ import * as _ from 'lodash'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { PotentialFilter, Corpus, SearchFilter, QueryModel } from '../models/index'; +import { Corpus, SearchFilter, QueryModel } from '../models/index'; @Component({ selector: 'ia-filter-manager', @@ -15,30 +15,22 @@ import { PotentialFilter, Corpus, SearchFilter, QueryModel } from '../models/ind export class FilterManagerComponent { @Input() corpus: Corpus; - @Input() - get queryModel(): QueryModel { - return this._queryModel; - } - set queryModel(model: QueryModel) { - this._queryModel = model; - this.setPotentialFilters(); - } - - public potentialFilters: PotentialFilter[] = []; - - private _corpus: Corpus; - private _queryModel: QueryModel; + @Input() queryModel: QueryModel; constructor() { } get activeFilters(): SearchFilter[] { - return this.queryModel.filters; + return this.queryModel?.activeFilters; + } + + get filters(): SearchFilter[] { + return this.queryModel?.filters; } get anyActiveFilters$(): Observable { - if (this.potentialFilters) { - const statuses = this.potentialFilters.map(filter => filter.useAsFilter); + if (this.filters) { + const statuses = this.filters.map(filter => filter.active); return combineLatest(statuses).pipe( map(values => _.some(values)), ); @@ -46,33 +38,27 @@ export class FilterManagerComponent { } get anyNonDefaultFilters$(): Observable { - if (this.potentialFilters) { - const statuses = this.potentialFilters.map(filter => filter.filter.isDefault$); + if (this.filters) { + const statuses = this.filters.map(filter => filter.isDefault$); return combineLatest(statuses).pipe( map(values => !_.every(values)), ); } } - setPotentialFilters() { - if (this.corpus && this.queryModel) { - this.potentialFilters = this.corpus.fields.map(field => new PotentialFilter(field, this.queryModel)); - } - } - public toggleActiveFilters() { if (this.activeFilters.length) { - this.potentialFilters.forEach(filter => filter.deactivate()); + this.filters.forEach(filter => filter.deactivate()); } else { // if we don't have active filters, set all filters to active which don't use default data - const filtersWithSettings = this.potentialFilters.filter(pFilter => - !_.isEqual(pFilter.filter.currentData, pFilter.filter.defaultData)); + const filtersWithSettings = this.filters.filter(filter => + !_.isEqual(filter.currentData, filter.defaultData)); filtersWithSettings.forEach(filter => filter.toggle()); } } public resetAllFilters() { - this.potentialFilters.forEach(filter => { + this.filters.forEach(filter => { filter.reset(); }); } diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index 5e00b154d..f7a7cfbb4 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -20,8 +20,8 @@ describe('MultipleChoiceFilterComponent', () => { component = fixture.componentInstance; const corpus = _.cloneDeep(mockCorpus); corpus.fields.push(mockFieldMultipleChoice); - const query = new QueryModel(corpus); - component.filter = new PotentialFilter(mockFieldMultipleChoice, query); + component.queryModel = new QueryModel(corpus); + component.filter = component.queryModel.filterForField(mockFieldMultipleChoice); fixture.detectChanges(); }); diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index d936ae569..d1452f775 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -25,8 +25,8 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent private async getOptions(): Promise { const optionCount = (this.filter.corpusField.filterOptions as MultipleChoiceFilterOptions).option_count; const aggregator = {name: this.filter.corpusField.name, size: optionCount}; - const queryModel = this.filter.queryModel.clone(); - queryModel.removeFilter(this.filter.filter); // exclude the choices for this filter + const queryModel = this.queryModel.clone(); + queryModel.filterForField(this.filter.corpusField).deactivate(); this.searchService.aggregateSearch(queryModel.corpus, queryModel, [aggregator]).then( response => response.aggregations[this.filter.corpusField.name]).then(aggregations => this.options = _.sortBy( diff --git a/frontend/src/app/filter/range-filter.component.spec.ts b/frontend/src/app/filter/range-filter.component.spec.ts index bfb1340bc..7b8c6ed6a 100644 --- a/frontend/src/app/filter/range-filter.component.spec.ts +++ b/frontend/src/app/filter/range-filter.component.spec.ts @@ -17,9 +17,9 @@ describe('RangeFilterComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(RangeFilterComponent); component = fixture.componentInstance; - const query = new QueryModel(mockCorpus3); - component.filter = new PotentialFilter(mockField3, query); - component.filter.filter.set({min: 1984, max: 1984}); + component.queryModel = new QueryModel(mockCorpus3); + component.filter = component.queryModel.filterForField(mockField3); + component.filter.set({min: 1984, max: 1984}); fixture.detectChanges(); }); diff --git a/frontend/src/app/models/search-filter.ts b/frontend/src/app/models/search-filter.ts index e9997f4e5..79c5c579b 100644 --- a/frontend/src/app/models/search-filter.ts +++ b/frontend/src/app/models/search-filter.ts @@ -23,6 +23,10 @@ abstract class AbstractSearchFilter { this.active = new BehaviorSubject(false); } + get filterType() { + return this.corpusField.filterOptions?.name; + } + get currentData() { return this.data?.value; } @@ -35,11 +39,11 @@ abstract class AbstractSearchFilter { get adHoc() { - return _.isUndefined(this.corpusField.filterOptions); + return !(this.corpusField.filterOptions); } get description() { - if (this.corpusField?.filterOptions.description) { + if (this.corpusField?.filterOptions?.description) { return this.corpusField.filterOptions.description; } else { return `Filter results based on ${this.corpusField.displayName}`; From 3f0aee465fd64bcdffcdf7d9647f679b62e39120 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 16:27:34 +0200 Subject: [PATCH 222/262] remove potentialfilter class --- .../app/filter/date-filter.component.spec.ts | 2 +- .../multiple-choice-filter.component.spec.ts | 2 +- .../app/filter/range-filter.component.spec.ts | 2 +- .../src/app/models/filter-management.spec.ts | 51 ----------------- frontend/src/app/models/filter-management.ts | 56 ------------------- frontend/src/app/models/index.ts | 1 - 6 files changed, 3 insertions(+), 111 deletions(-) delete mode 100644 frontend/src/app/models/filter-management.spec.ts delete mode 100644 frontend/src/app/models/filter-management.ts diff --git a/frontend/src/app/filter/date-filter.component.spec.ts b/frontend/src/app/filter/date-filter.component.spec.ts index 90b202d1c..c801cf112 100644 --- a/frontend/src/app/filter/date-filter.component.spec.ts +++ b/frontend/src/app/filter/date-filter.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { mockCorpus3, mockFieldDate } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; -import { PotentialFilter, QueryModel } from '../models'; +import { QueryModel } from '../models'; import { DateFilterComponent } from './date-filter.component'; diff --git a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts index f7a7cfbb4..b8f22213f 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.spec.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { mockCorpus, mockFieldMultipleChoice } from '../../mock-data/corpus'; import { commonTestBed } from '../common-test-bed'; -import { PotentialFilter, QueryModel } from '../models'; +import { QueryModel } from '../models'; import { MultipleChoiceFilterComponent } from './multiple-choice-filter.component'; import * as _ from 'lodash'; diff --git a/frontend/src/app/filter/range-filter.component.spec.ts b/frontend/src/app/filter/range-filter.component.spec.ts index 7b8c6ed6a..95f948f06 100644 --- a/frontend/src/app/filter/range-filter.component.spec.ts +++ b/frontend/src/app/filter/range-filter.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { commonTestBed } from '../common-test-bed'; import { RangeFilterComponent } from './range-filter.component'; -import { PotentialFilter, QueryModel } from '../models'; +import { QueryModel } from '../models'; import { mockCorpus3, mockField3 } from '../../mock-data/corpus'; describe('RangeFilterComponent', () => { diff --git a/frontend/src/app/models/filter-management.spec.ts b/frontend/src/app/models/filter-management.spec.ts deleted file mode 100644 index 5cfdb09fd..000000000 --- a/frontend/src/app/models/filter-management.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { mockCorpus, mockCorpus3, mockField, mockFieldDate, mockFieldMultipleChoice } from '../../mock-data/corpus'; -import { PotentialFilter } from './filter-management'; -import { QueryModel } from './query'; - -describe('PotentialFilter', () => { - it('should create', () => { - const field = mockField; - const query = new QueryModel(mockCorpus); - const potentialFilter = new PotentialFilter(field, query); - expect(potentialFilter).toBeTruthy(); - }); - - it('should toggle', () => { - const field = mockField; - const query = new QueryModel(mockCorpus); - const potentialFilter = new PotentialFilter(field, query); - potentialFilter.filter.set(true); - - expect(query.activeFilters.length).toBe(1); - potentialFilter.toggle(); - expect(query.activeFilters.length).toBe(0); - potentialFilter.toggle(); - expect(query.activeFilters.length).toBe(1); - }); - - it('should deactivate when a date filter resets', () => { - const field = mockFieldDate; - const query = new QueryModel(mockCorpus3); - const potentialFilter = new PotentialFilter(field, query); - - potentialFilter.filter.setToValue('Jan 1 1850'); - potentialFilter.activate(); - expect(query.activeFilters.length).toBe(1); - - potentialFilter.filter.reset(); - expect(query.activeFilters.length).toBe(0); - }); - - it('should deactivate when a multiple choice filter resets', () => { - const field = mockFieldMultipleChoice; - const query = new QueryModel(mockCorpus3); - const potentialFilter = new PotentialFilter(field, query); - - potentialFilter.filter.setToValue('test'); - potentialFilter.activate(); - expect(query.activeFilters.length).toBe(1); - - potentialFilter.filter.set([]); - expect(query.activeFilters.length).toBe(0); - }); -}); diff --git a/frontend/src/app/models/filter-management.ts b/frontend/src/app/models/filter-management.ts deleted file mode 100644 index ad9edeaed..000000000 --- a/frontend/src/app/models/filter-management.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as _ from 'lodash'; -import { CorpusField } from './corpus'; -import { QueryModel } from './query'; -import { SearchFilter } from './search-filter'; -import { SearchFilterType } from './search-filter-options'; -import { Observable } from 'rxjs-compat'; - -export class PotentialFilter { - filter: SearchFilter; - useAsFilter: Observable; - description: string; - adHoc?: boolean; - - constructor(public corpusField: CorpusField, public queryModel: QueryModel) { - if (queryModel.filterForField(corpusField)) { - this.filter = queryModel.filterForField(corpusField); - } else { - this.filter = corpusField.makeSearchFilter(); - this.queryModel.addFilter(this.filter); - } - - this.useAsFilter = this.filter.active.asObservable(); - - if (!corpusField.filterOptions) { - this.description = `View results from this ${corpusField.displayName}`; - this.adHoc = true; - } else { - this.description = corpusField.filterOptions.description; - this.adHoc = false; - } - } - - get filterType(): SearchFilterType { - return this.corpusField.filterOptions?.name; - } - - toggle() { - this.filter.toggle(); - } - - deactivate() { - this.filter.deactivate(); - } - - activate() { - this.filter.activate(); - } - - set(data: any) { - this.filter.set(data); - } - - reset() { - this.filter.reset(); - } -} diff --git a/frontend/src/app/models/index.ts b/frontend/src/app/models/index.ts index 06b8a1a66..764f71ba5 100644 --- a/frontend/src/app/models/index.ts +++ b/frontend/src/app/models/index.ts @@ -3,7 +3,6 @@ export * from './found-document'; export * from './query'; export * from './search-filter'; export * from './search-filter-options'; -export * from './filter-management'; export * from './search-results'; export * from './sort'; export * from './user'; From 3d70e0843dee6a20af3a3c035664d1e8eff11ed3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 16:49:35 +0200 Subject: [PATCH 223/262] proper updating of multiple choice aggregation --- .../src/app/filter/base-filter.component.ts | 37 ++++++++++++------- .../filter/filter-manager.component.spec.ts | 4 +- .../app/filter/filter-manager.component.ts | 4 +- .../multiple-choice-filter.component.ts | 33 ++++++++++------- frontend/src/app/search/search.component.html | 2 +- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 451ec88d1..0bd547889 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -1,8 +1,8 @@ -/* eslint-disable @typescript-eslint/member-ordering */ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import * as _ from 'lodash'; import { QueryModel, SearchFilter } from '../models/index'; +import { Subscription } from 'rxjs'; /** * Filter component receives the corpus fields containing search filters as input @@ -11,8 +11,11 @@ import { QueryModel, SearchFilter } from '../models/index'; @Component({ template: '' }) -export abstract class BaseFilterComponent { - private _filter: SearchFilter; +export abstract class BaseFilterComponent implements OnChanges { + @Input() filter: SearchFilter; + @Input() queryModel: QueryModel; + + private queryModelSubscription: Subscription; constructor() { } @@ -20,18 +23,22 @@ export abstract class BaseFilterComponent { return this.filter?.currentData; } - @Input() - get filter() { - return this._filter; - } - set filter(filter: SearchFilter) { - this._filter = filter; - this.onFilterSet(filter); + ngOnChanges(changes: SimpleChanges): void { + if (changes.filtter) { + this.onFilterSet(this.filter); + } + + if (changes.queryModel) { + if (this.queryModelSubscription) { + this.queryModelSubscription.unsubscribe(); + } + this.queryModelSubscription = this.queryModel.update.subscribe(() => + this.onQueryModelUpdate() + ); + this.onQueryModelUpdate(); // run update immediately + } } - @Input() queryModel: QueryModel; - - /** * Trigger a change event. */ @@ -41,4 +48,6 @@ export abstract class BaseFilterComponent { /** possible administration when the filter is set, e.g. setting data limits */ onFilterSet(filter): void {}; + + onQueryModelUpdate() {} } diff --git a/frontend/src/app/filter/filter-manager.component.spec.ts b/frontend/src/app/filter/filter-manager.component.spec.ts index c152787db..3604f7472 100644 --- a/frontend/src/app/filter/filter-manager.component.spec.ts +++ b/frontend/src/app/filter/filter-manager.component.spec.ts @@ -17,7 +17,7 @@ describe('FilterManagerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(FilterManagerComponent); component = fixture.componentInstance; - component.corpus = mockCorpus; + const corpus = mockCorpus; component.queryModel = new QueryModel(mockCorpus); fixture.detectChanges(); }); @@ -28,13 +28,11 @@ describe('FilterManagerComponent', () => { }); it('resets filters when corpus changes', () => { - component.corpus = mockCorpus2; component.queryModel = new QueryModel(mockCorpus2); fixture.detectChanges(); expect(component.filters.length).toEqual(1); expect(component.filters[0].adHoc).toBeTrue(); - component.corpus = mockCorpus; component.queryModel = new QueryModel(mockCorpus); fixture.detectChanges(); expect(component.filters.length).toEqual(2); diff --git a/frontend/src/app/filter/filter-manager.component.ts b/frontend/src/app/filter/filter-manager.component.ts index ccb6232e9..0121fdec8 100644 --- a/frontend/src/app/filter/filter-manager.component.ts +++ b/frontend/src/app/filter/filter-manager.component.ts @@ -5,7 +5,7 @@ import * as _ from 'lodash'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Corpus, SearchFilter, QueryModel } from '../models/index'; +import { SearchFilter, QueryModel } from '../models/index'; @Component({ selector: 'ia-filter-manager', @@ -13,8 +13,6 @@ import { Corpus, SearchFilter, QueryModel } from '../models/index'; styleUrls: ['./filter-manager.component.scss'] }) export class FilterManagerComponent { - @Input() corpus: Corpus; - @Input() queryModel: QueryModel; constructor() { diff --git a/frontend/src/app/filter/multiple-choice-filter.component.ts b/frontend/src/app/filter/multiple-choice-filter.component.ts index d1452f775..da2ac3200 100644 --- a/frontend/src/app/filter/multiple-choice-filter.component.ts +++ b/frontend/src/app/filter/multiple-choice-filter.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; import * as _ from 'lodash'; import { BaseFilterComponent } from './base-filter.component'; -import { MultipleChoiceFilter, MultipleChoiceFilterOptions } from '../models'; +import { MultipleChoiceFilterOptions } from '../models'; import { SearchService } from '../services'; @Component({ @@ -18,21 +18,28 @@ export class MultipleChoiceFilterComponent extends BaseFilterComponent super(); } - onFilterSet(filter: MultipleChoiceFilter): void { + onFilterSet(): void { + this.getOptions(); + } + + onQueryModelUpdate(): void { this.getOptions(); } private async getOptions(): Promise { - const optionCount = (this.filter.corpusField.filterOptions as MultipleChoiceFilterOptions).option_count; - const aggregator = {name: this.filter.corpusField.name, size: optionCount}; - const queryModel = this.queryModel.clone(); - queryModel.filterForField(this.filter.corpusField).deactivate(); - this.searchService.aggregateSearch(queryModel.corpus, queryModel, [aggregator]).then( - response => response.aggregations[this.filter.corpusField.name]).then(aggregations => - this.options = _.sortBy( - aggregations.map(x => ({ label: x.key, value: x.key, doc_count: x.doc_count })), - o => o.label - ) - ).catch(() => this.options = []); + if (this.filter && this.queryModel) { + const optionCount = (this.filter.corpusField.filterOptions as MultipleChoiceFilterOptions).option_count; + const aggregator = {name: this.filter.corpusField.name, size: optionCount}; + const queryModel = this.queryModel.clone(); + queryModel.filterForField(this.filter.corpusField).deactivate(); + this.searchService.aggregateSearch(queryModel.corpus, queryModel, [aggregator]).then( + response => response.aggregations[this.filter.corpusField.name]).then(aggregations => + this.options = _.sortBy( + aggregations.map(x => ({ label: x.key, value: x.key, doc_count: x.doc_count })), + o => o.label + ) + ).catch(() => this.options = []); + + } } } diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index fdd6ff55d..192b4c4ad 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -49,7 +49,7 @@
- +
From 4f8e61bb27e16883b5035cd74ebb2429be8a602b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 17:14:13 +0200 Subject: [PATCH 224/262] remove console.log --- frontend/src/app/visualization/barchart/barchart.directive.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts index 00dc15a58..b8c680f63 100644 --- a/frontend/src/app/visualization/barchart/barchart.directive.ts +++ b/frontend/src/app/visualization/barchart/barchart.directive.ts @@ -414,7 +414,6 @@ export abstract class BarchartDirective getSeriesDocumentData( series: BarchartSeries, queryModel: QueryModel = this.queryModel, setSearchRatio = true ): Promise> { - console.log(queryModel); const queryModelCopy = this.queryModelForSeries(series, queryModel); return this.requestSeriesDocCounts(queryModelCopy).then(result => From ca5c327daf85673f5ebee628617d02ce16291f2c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 17:19:44 +0200 Subject: [PATCH 225/262] rewrite migration history --- ..._to_es_query.py => 0003_convert_to_es_query.py} | 2 +- backend/api/migrations/0003_merge_20230412_1706.py | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) rename backend/api/migrations/{0002_convert_to_es_query.py => 0003_convert_to_es_query.py} (95%) delete mode 100644 backend/api/migrations/0003_merge_20230412_1706.py diff --git a/backend/api/migrations/0002_convert_to_es_query.py b/backend/api/migrations/0003_convert_to_es_query.py similarity index 95% rename from backend/api/migrations/0002_convert_to_es_query.py rename to backend/api/migrations/0003_convert_to_es_query.py index 04b3142cd..4a45aacf7 100644 --- a/backend/api/migrations/0002_convert_to_es_query.py +++ b/backend/api/migrations/0003_convert_to_es_query.py @@ -21,7 +21,7 @@ def convert_query_format_to_query_model(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('api', '0001_initial'), + ('api', '0002_alter_query_started'), ] operations = [ diff --git a/backend/api/migrations/0003_merge_20230412_1706.py b/backend/api/migrations/0003_merge_20230412_1706.py deleted file mode 100644 index 4ac979956..000000000 --- a/backend/api/migrations/0003_merge_20230412_1706.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 4.1.5 on 2023-04-12 15:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_alter_query_started'), - ('api', '0002_convert_to_es_query'), - ] - - operations = [ - ] From 83d531608a94b6d4db7338497467284604e19da6 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 15:03:20 +0200 Subject: [PATCH 226/262] start tag app --- backend/ianalyzer/common_settings.py | 1 + backend/tag/__init__.py | 0 backend/tag/admin.py | 3 +++ backend/tag/apps.py | 6 ++++++ backend/tag/conftest.py | 0 backend/tag/migrations/__init__.py | 0 backend/tag/models.py | 3 +++ backend/tag/views.py | 3 +++ 8 files changed, 16 insertions(+) create mode 100644 backend/tag/__init__.py create mode 100644 backend/tag/admin.py create mode 100644 backend/tag/apps.py create mode 100644 backend/tag/conftest.py create mode 100644 backend/tag/migrations/__init__.py create mode 100644 backend/tag/models.py create mode 100644 backend/tag/views.py diff --git a/backend/ianalyzer/common_settings.py b/backend/ianalyzer/common_settings.py index de63f8347..c758d913f 100644 --- a/backend/ianalyzer/common_settings.py +++ b/backend/ianalyzer/common_settings.py @@ -36,6 +36,7 @@ 'download', 'wordmodels', 'media', + 'tag', ] SITE_ID = 1 diff --git a/backend/tag/__init__.py b/backend/tag/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tag/admin.py b/backend/tag/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/tag/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/tag/apps.py b/backend/tag/apps.py new file mode 100644 index 000000000..72862e535 --- /dev/null +++ b/backend/tag/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TagConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tag' diff --git a/backend/tag/conftest.py b/backend/tag/conftest.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tag/migrations/__init__.py b/backend/tag/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tag/models.py b/backend/tag/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/backend/tag/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/tag/views.py b/backend/tag/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/backend/tag/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From df0d79713de50f95a7d433585d5d60dcd292420c Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 7 Jun 2023 15:12:14 +0200 Subject: [PATCH 227/262] add tag model --- backend/tag/conftest.py | 8 ++++++ backend/tag/migrations/0001_initial.py | 36 ++++++++++++++++++++++++++ backend/tag/models.py | 28 ++++++++++++++++++++ backend/tag/tests/test_tag_models.py | 22 ++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 backend/tag/migrations/0001_initial.py create mode 100644 backend/tag/tests/test_tag_models.py diff --git a/backend/tag/conftest.py b/backend/tag/conftest.py index e69de29bb..9cd001804 100644 --- a/backend/tag/conftest.py +++ b/backend/tag/conftest.py @@ -0,0 +1,8 @@ +import pytest +from addcorpus.models import Corpus + +@pytest.fixture() +def mock_corpus(db): + name = 'mock-corpus' + Corpus.objects.create(name=name) + return name diff --git a/backend/tag/migrations/0001_initial.py b/backend/tag/migrations/0001_initial.py new file mode 100644 index 000000000..1954270fa --- /dev/null +++ b/backend/tag/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.9 on 2023-06-07 13:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('addcorpus', '0002_alter_corpus_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=512)), + ('description', models.TextField(blank=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='TagInstance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('document_id', models.CharField(max_length=512)), + ('corpus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_instances', to='addcorpus.corpus', to_field='name')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='tag.tag')), + ], + ), + ] diff --git a/backend/tag/models.py b/backend/tag/models.py index 71a836239..5652e9e95 100644 --- a/backend/tag/models.py +++ b/backend/tag/models.py @@ -1,3 +1,31 @@ from django.db import models +from addcorpus.models import Corpus +from users.models import CustomUser + # Create your models here. + +class Tag(models.Model): + name = models.CharField(blank=False, null=False, max_length=512) + description = models.TextField(blank=True, null=False) + user = models.ForeignKey( + to=CustomUser, + related_name='tags', + on_delete=models.CASCADE, + null=False, + ) + +class TagInstance(models.Model): + tag = models.ForeignKey( + to=Tag, + related_name='instances', + on_delete=models.CASCADE, + null=False + ) + corpus = models.ForeignKey( + to=Corpus, + on_delete=models.CASCADE, + to_field='name', + related_name='tag_instances', + ) + document_id = models.CharField(blank=False, null=False, max_length=512) diff --git a/backend/tag/tests/test_tag_models.py b/backend/tag/tests/test_tag_models.py new file mode 100644 index 000000000..fa81d030b --- /dev/null +++ b/backend/tag/tests/test_tag_models.py @@ -0,0 +1,22 @@ +from addcorpus.models import Corpus +from tag.models import Tag, TagInstance + +def test_tag_models(db, auth_user, mock_corpus): + tag = Tag.objects.create( + name='fascinating', + description='some very interesting documents', + user=auth_user + ) + + assert len(auth_user.tags.all()) == 1 + + corpus = Corpus.objects.get(name=mock_corpus) + + for doc in ['1', '2', '3']: + TagInstance.objects.create( + tag=tag, + corpus=corpus, + document_id=doc, + ) + + assert len(tag.instances.all()) == 3 From d94630756771955aba20b06af76661c7613cac86 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 17:22:40 +0200 Subject: [PATCH 228/262] add mock corpus --- backend/ianalyzer/settings_test.py | 3 ++- backend/tag/conftest.py | 7 +++---- backend/tag/tests/data/test_data.csv | 7 +++++++ backend/tag/tests/tag_mock_corpus.py | 31 ++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 backend/tag/tests/data/test_data.csv create mode 100644 backend/tag/tests/tag_mock_corpus.py diff --git a/backend/ianalyzer/settings_test.py b/backend/ianalyzer/settings_test.py index fd29c0508..6db553733 100644 --- a/backend/ianalyzer/settings_test.py +++ b/backend/ianalyzer/settings_test.py @@ -9,7 +9,8 @@ def path_in_testdir(app, *path_from_app_tests): 'multilingual-mock-corpus': path_in_testdir('download', 'mock_corpora', 'multilingual_mock_corpus.py'), 'times': os.path.join(BASE_DIR, 'corpora', 'times', 'times.py'), 'media-mock-corpus': path_in_testdir('media', 'media_mock_corpus.py'), - 'mock-csv-corpus': path_in_testdir('addcorpus', 'mock_csv_corpus.py') + 'mock-csv-corpus': path_in_testdir('addcorpus', 'mock_csv_corpus.py'), + 'tagging-mock-corpus': path_in_testdir('tag', 'tag_mock_corpus.py'), } TIMES_DATA = path_in_testdir('addcorpus', '') diff --git a/backend/tag/conftest.py b/backend/tag/conftest.py index 9cd001804..dc98281fe 100644 --- a/backend/tag/conftest.py +++ b/backend/tag/conftest.py @@ -1,8 +1,7 @@ import pytest -from addcorpus.models import Corpus +from addcorpus.load_corpus import load_all_corpora @pytest.fixture() def mock_corpus(db): - name = 'mock-corpus' - Corpus.objects.create(name=name) - return name + load_all_corpora() + return 'tagging-mock-corpus' diff --git a/backend/tag/tests/data/test_data.csv b/backend/tag/tests/data/test_data.csv new file mode 100644 index 000000000..620276085 --- /dev/null +++ b/backend/tag/tests/data/test_data.csv @@ -0,0 +1,7 @@ +id,content +1,"Text: the final frontier..." +2,"There are the voyages of the starship I-Analyzer." +3,"Its continuing mission:" +4,"To explore strange new documents," +5,"To seek out new texts, and new corpora," +6,"To boldy go where no text-mining application has gone before!" diff --git a/backend/tag/tests/tag_mock_corpus.py b/backend/tag/tests/tag_mock_corpus.py new file mode 100644 index 000000000..cc5c2bef6 --- /dev/null +++ b/backend/tag/tests/tag_mock_corpus.py @@ -0,0 +1,31 @@ +import os +import datetime + +from addcorpus.corpus import CSVCorpus, Field +from addcorpus.extract import CSV +from addcorpus.es_mappings import keyword_mapping, main_content_mapping + +here = os.path.abspath(os.path.dirname(__file__)) + +class TaggingMockCorpus(CSVCorpus): + title = 'Tagging Mock Corpus' + description = 'Mock corpus for tagging' + es_index = 'tagging-mock-corpus' + min_date = datetime.datetime(year=1, month=1, day=1) + max_date = datetime.datetime(year=2022, month=12, day=31) + image = 'nothing.jpeg' + data_directory = os.path.join(here, 'csv_example') + + + fields = [ + Field( + name='id', + extractor=CSV('id'), + es_mapping=keyword_mapping() + ), + Field( + name='content', + extractor=CSV('content'), + es_mapping=main_content_mapping(), + ) + ] From 73f3febe4e02211af8f938698ccfd032204e2f41 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Mon, 12 Jun 2023 17:49:09 +0200 Subject: [PATCH 229/262] make document ids a json field r --- backend/tag/conftest.py | 25 +++++++++++++++ backend/tag/migrations/0001_initial.py | 7 ++-- backend/tag/models.py | 9 ++++-- backend/tag/tests/test_tag_models.py | 44 +++++++++++++++++--------- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/backend/tag/conftest.py b/backend/tag/conftest.py index dc98281fe..780f0787b 100644 --- a/backend/tag/conftest.py +++ b/backend/tag/conftest.py @@ -1,7 +1,32 @@ import pytest from addcorpus.load_corpus import load_all_corpora +from tag.models import Tag, TagInstance +from addcorpus.models import Corpus @pytest.fixture() def mock_corpus(db): load_all_corpora() return 'tagging-mock-corpus' + +@pytest.fixture() +def auth_user_tag(db, auth_user): + tag = Tag.objects.create( + name='fascinating', + description='some very interesting documents', + user=auth_user + ) + + return tag + +@pytest.fixture() +def tagged_documents(auth_user_tag, mock_corpus): + corpus = Corpus.objects.get(name=mock_corpus) + docs = ['1', '2', '3'] + + tagged = TagInstance.objects.create( + tag=auth_user_tag, + corpus=corpus, + document_ids=docs, + ) + + return tagged, docs diff --git a/backend/tag/migrations/0001_initial.py b/backend/tag/migrations/0001_initial.py index 1954270fa..577f3a17c 100644 --- a/backend/tag/migrations/0001_initial.py +++ b/backend/tag/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 4.1.9 on 2023-06-07 13:02 +# Generated by Django 4.1.9 on 2023-06-12 15:56 from django.conf import settings +import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -10,8 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('addcorpus', '0002_alter_corpus_options'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('addcorpus', '0002_alter_corpus_options'), ] operations = [ @@ -28,7 +29,7 @@ class Migration(migrations.Migration): name='TagInstance', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('document_id', models.CharField(max_length=512)), + ('document_ids', models.JSONField(default=list, validators=[django.core.validators.MaxLengthValidator(100)])), ('corpus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_instances', to='addcorpus.corpus', to_field='name')), ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='tag.tag')), ], diff --git a/backend/tag/models.py b/backend/tag/models.py index 5652e9e95..e7274d8ba 100644 --- a/backend/tag/models.py +++ b/backend/tag/models.py @@ -1,9 +1,10 @@ from django.db import models +from django.core.validators import MaxLengthValidator from addcorpus.models import Corpus from users.models import CustomUser -# Create your models here. +DOCS_PER_TAG_LIMIT = 100 class Tag(models.Model): name = models.CharField(blank=False, null=False, max_length=512) @@ -28,4 +29,8 @@ class TagInstance(models.Model): to_field='name', related_name='tag_instances', ) - document_id = models.CharField(blank=False, null=False, max_length=512) + document_ids = models.JSONField( + default=list, + null=False, + validators=[MaxLengthValidator(DOCS_PER_TAG_LIMIT)], + ) diff --git a/backend/tag/tests/test_tag_models.py b/backend/tag/tests/test_tag_models.py index fa81d030b..e2260b4b0 100644 --- a/backend/tag/tests/test_tag_models.py +++ b/backend/tag/tests/test_tag_models.py @@ -1,22 +1,36 @@ from addcorpus.models import Corpus -from tag.models import Tag, TagInstance - -def test_tag_models(db, auth_user, mock_corpus): - tag = Tag.objects.create( - name='fascinating', - description='some very interesting documents', - user=auth_user - ) +from tag.models import TagInstance, DOCS_PER_TAG_LIMIT +import pytest +from django.core.exceptions import ValidationError +def test_tag_models(db, auth_user, auth_user_tag, tagged_documents): assert len(auth_user.tags.all()) == 1 + instance, docs = tagged_documents + + assert len(auth_user_tag.instances.all()) == 1 + assert len(instance.document_ids) == len(docs) + +def test_tag_lookup(mock_corpus, tagged_documents): + instance, docs = tagged_documents + corpus = Corpus.objects.get(name=mock_corpus) + + for doc in docs: + assert TagInstance.objects.filter(corpus=corpus, document_ids__contains=doc) + + assert not TagInstance.objects.filter(corpus=corpus, document_ids__contains='not_tagged') + +def test_max_length(db, mock_corpus, auth_user_tag): corpus = Corpus.objects.get(name=mock_corpus) + instance = TagInstance.objects.create(tag=auth_user_tag, corpus=corpus) + + for i in range(DOCS_PER_TAG_LIMIT): + instance.document_ids.append(str(i)) + instance.save() + instance.full_clean() # should validate without error - for doc in ['1', '2', '3']: - TagInstance.objects.create( - tag=tag, - corpus=corpus, - document_id=doc, - ) + instance.document_ids.append('too_much') + instance.save() - assert len(tag.instances.all()) == 3 + with pytest.raises(ValidationError): + instance.full_clean() From da2d6d0ebbf94a9857357ae6fbf7fb5358866480 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 13 Jun 2023 12:24:01 +0200 Subject: [PATCH 230/262] model changes based on PR suggestions --- backend/tag/migrations/0001_initial.py | 12 ++++++++---- backend/tag/models.py | 23 +++++++++++++++++------ backend/tag/tests/test_tag_models.py | 4 ++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/backend/tag/migrations/0001_initial.py b/backend/tag/migrations/0001_initial.py index 577f3a17c..6ff68a4ec 100644 --- a/backend/tag/migrations/0001_initial.py +++ b/backend/tag/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 4.1.9 on 2023-06-12 15:56 +# Generated by Django 4.1.9 on 2023-06-13 10:20 from django.conf import settings -import django.core.validators +import django.contrib.postgres.fields from django.db import migrations, models import django.db.models.deletion @@ -11,8 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('addcorpus', '0002_alter_corpus_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -29,9 +29,13 @@ class Migration(migrations.Migration): name='TagInstance', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('document_ids', models.JSONField(default=list, validators=[django.core.validators.MaxLengthValidator(100)])), + ('document_ids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=512), default=list, size=500)), ('corpus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_instances', to='addcorpus.corpus', to_field='name')), ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='tag.tag')), ], ), + migrations.AddConstraint( + model_name='tag', + constraint=models.UniqueConstraint(fields=('user', 'name'), name='unique_name_for_user'), + ), ] diff --git a/backend/tag/models.py b/backend/tag/models.py index e7274d8ba..51bd9784f 100644 --- a/backend/tag/models.py +++ b/backend/tag/models.py @@ -1,21 +1,27 @@ from django.db import models -from django.core.validators import MaxLengthValidator +from django.db.models.constraints import UniqueConstraint +from django.contrib.postgres.fields import ArrayField from addcorpus.models import Corpus -from users.models import CustomUser +from django.conf import settings -DOCS_PER_TAG_LIMIT = 100 +DOCS_PER_TAG_LIMIT = 500 class Tag(models.Model): name = models.CharField(blank=False, null=False, max_length=512) description = models.TextField(blank=True, null=False) user = models.ForeignKey( - to=CustomUser, + to=settings.AUTH_USER_MODEL, related_name='tags', on_delete=models.CASCADE, null=False, ) + class Meta: + constraints = [ + UniqueConstraint(fields=['user', 'name'], name='unique_name_for_user') + ] + class TagInstance(models.Model): tag = models.ForeignKey( to=Tag, @@ -29,8 +35,13 @@ class TagInstance(models.Model): to_field='name', related_name='tag_instances', ) - document_ids = models.JSONField( + document_ids = ArrayField( + models.CharField( + blank=False, + null=False, + max_length=512, + ), default=list, null=False, - validators=[MaxLengthValidator(DOCS_PER_TAG_LIMIT)], + size=DOCS_PER_TAG_LIMIT, ) diff --git a/backend/tag/tests/test_tag_models.py b/backend/tag/tests/test_tag_models.py index e2260b4b0..dfc776aa9 100644 --- a/backend/tag/tests/test_tag_models.py +++ b/backend/tag/tests/test_tag_models.py @@ -16,9 +16,9 @@ def test_tag_lookup(mock_corpus, tagged_documents): corpus = Corpus.objects.get(name=mock_corpus) for doc in docs: - assert TagInstance.objects.filter(corpus=corpus, document_ids__contains=doc) + assert TagInstance.objects.filter(corpus=corpus, document_ids__contains=[doc]) - assert not TagInstance.objects.filter(corpus=corpus, document_ids__contains='not_tagged') + assert not TagInstance.objects.filter(corpus=corpus, document_ids__contains=['not_tagged']) def test_max_length(db, mock_corpus, auth_user_tag): corpus = Corpus.objects.get(name=mock_corpus) From e7a291f05f32659e6e72ecc3b7eb2c9451e84548 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 13 Jun 2023 15:09:25 +0200 Subject: [PATCH 231/262] fix flask data transfer test --- backend/ianalyzer/tests/test_flask_data_transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ianalyzer/tests/test_flask_data_transfer.py b/backend/ianalyzer/tests/test_flask_data_transfer.py index 0618e65ef..179ae8a01 100644 --- a/backend/ianalyzer/tests/test_flask_data_transfer.py +++ b/backend/ianalyzer/tests/test_flask_data_transfer.py @@ -70,7 +70,7 @@ def test_save_legacy_user(db): users = CustomUser.objects.all() assert len(users) == 4 - admin = users[0] + admin = CustomUser.objects.get(username='admin') assert admin.username == 'admin' assert admin.email == 'admin@ianalyzer.nl' assert admin.is_superuser From 962866edc3a5e25613ba1ae2ac77410f87fb8205 Mon Sep 17 00:00:00 2001 From: Berit Date: Wed, 14 Jun 2023 10:51:05 +0200 Subject: [PATCH 232/262] Update frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts Co-authored-by: Luka van der Plas <43678097+lukavdplas@users.noreply.github.com> --- .../word-models/similarity-chart/similarity-chart.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts index 7d3439140..4644f3b88 100644 --- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts +++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts @@ -108,7 +108,7 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { } formatLabel(value: number): string { - return this.timeIntervals.filter(t => t.startsWith(value.toString()))[0] + return this.timeIntervals.find(t => t.startsWith(value.toString())) } getStartTime(time: string): number { From 1bc4ec7b00a3c026228b1095e5c14defced45c7a Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 14 Jun 2023 10:59:51 +0200 Subject: [PATCH 233/262] fix test --- .../similarity-chart/similarity-chart.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.spec.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.spec.ts index cd893d774..869bcae4a 100644 --- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.spec.ts +++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.spec.ts @@ -36,6 +36,7 @@ describe('SimilarityChartComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SimilarityChartComponent); component = fixture.componentInstance; + component.timeIntervals = EXAMPLE_DATA.labels; fixture.detectChanges(); }); From 0b9d1ab4a6b6b1547bafa9163e172afcf4ec8e73 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 14 Jun 2023 12:32:13 +0200 Subject: [PATCH 234/262] include types; use average instead of start value --- .../similarity-chart.component.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts index 4644f3b88..2b434c6f3 100644 --- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts +++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; -import { Chart, ChartData, ChartOptions, Filler, TooltipItem } from 'chart.js'; +import { Chart, ChartData, ChartOptions, ChartType, Filler, TooltipItem } from 'chart.js'; import Zoom from 'chartjs-plugin-zoom'; import * as _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; @@ -31,7 +31,8 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { tableHeaders: FreqTableHeaders; tableData: WordSimilarity[]; - startTimeIntervals: Number[]; + + averages: Number[]; graphStyle = new BehaviorSubject<'line'|'bar'>('line'); @@ -108,16 +109,19 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { } formatLabel(value: number): string { - return this.timeIntervals.find(t => t.startsWith(value.toString())) + const index = this.averages.indexOf(value) + return this.timeIntervals[index]; } - getStartTime(time: string): number { - return parseInt(time.split('-')[0]) + getAverageTime(time: string): number { + const times = time.split('-').map(t => parseInt(t)); + const avg = Math.round(_.mean(times)); + return avg; } formatDataPoint(point: any, style: string): {x: number, y: number} | number { if (style == 'line') { - return {x: parseInt(point.time.split('-')[0]), y: point.similarity} + return {x: this.getAverageTime(point.time), y: point.similarity} } else { return point.similarity @@ -126,7 +130,7 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { /** convert array of word similarities to a chartData object */ makeChartData(data: WordSimilarity[], style: 'line'|'bar'): ChartData { - this.startTimeIntervals = this.timeIntervals.map(time => this.getStartTime(time)); + this.averages = this.timeIntervals.map(t => this.getAverageTime(t)); const allSeries = _.groupBy(data, point => point.key); const datasets = _.values(allSeries).map((series, datasetIndex) => { const label = series[0].key; @@ -190,7 +194,8 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { ticks: { stepSize: 1, autoSkip: true, - callback: (value: number): string | undefined => this.startTimeIntervals.includes(value) ? this.formatLabel(value) : undefined, + callback: (value: number): string | undefined => this.averages.includes( + value) ? this.formatLabel(value) : undefined, minRotation: 30 } }, @@ -209,8 +214,8 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy { tooltip: { displayColors: true, callbacks: { - title: (tooltipItem: any): string => { - return this.formatLabel(tooltipItem[0].parsed.x) + title: (tooltipItems: TooltipItem[]): string => { + return this.formatLabel(tooltipItems[0].parsed.x) }, labelColor: (tooltipItem: any): any => { const color = tooltipItem.dataset.borderColor; From 999f91e30697e2f16c4e1466b72fea63b32f81b4 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 14 Jun 2023 13:28:16 +0200 Subject: [PATCH 235/262] fix uniqueness function --- backend/corpora/dbnl/tests/test_dbnl_extraction.py | 12 +++++++++++- backend/corpora/dbnl/utils.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 6db6c91d6..f09aee2b9 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -4,7 +4,7 @@ from addcorpus.load_corpus import load_corpus from addcorpus.extract import XML -from corpora.dbnl.utils import append_to_tag, index_by_id +from corpora.dbnl.utils import append_to_tag, index_by_id, which_unique here = os.path.abspath(os.path.dirname(__file__)) @@ -18,6 +18,16 @@ def dbnl_corpus(settings): } return 'dbnl' +which_unique_testcases = [ + (['_ale002', '_ale002'], [True, False]), + (['proza', 'poëzie', 'proza'], [True, True, False]) +] + +@pytest.mark.parametrize(['items', 'uniquenesses'], which_unique_testcases) +def test_which_unique(items, uniquenesses): + result = list(which_unique(items)) + assert result == uniquenesses + def test_metadata_extraction(dbnl_corpus): corpus = load_corpus('dbnl_metadata') data = index_by_id(corpus.documents()) diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 3e146cefd..88e7338f5 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -38,7 +38,7 @@ def which_unique(items): iff `items[n]` is the first occurrence of that value. ''' - is_first = lambda n: n == 0 or items[n] not in items[:n-1] + is_first = lambda n: n == 0 or items[n] not in items[:n] return map(is_first, range(len(items))) def filter_values_by(values, which): From 8b91ab57eb3c84b87e8aca9e63625e36994b8782 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 14 Jun 2023 13:32:49 +0200 Subject: [PATCH 236/262] improve formatting of anonymous authors --- backend/corpora/dbnl/tests/data/titels_pd.csv | 2 ++ backend/corpora/dbnl/tests/test_dbnl_extraction.py | 8 ++++++-- backend/corpora/dbnl/utils.py | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/corpora/dbnl/tests/data/titels_pd.csv b/backend/corpora/dbnl/tests/data/titels_pd.csv index df776e670..9b2239e30 100644 --- a/backend/corpora/dbnl/tests/data/titels_pd.csv +++ b/backend/corpora/dbnl/tests/data/titels_pd.csv @@ -10,3 +10,5 @@ ti_id|titel|vols|jaar|druk|ppn_o|bibliotheek|categorie|_jaar|pers_id|voornaam|vo "maer005sing01"|"Het singende nachtegaeltje"||"1671"|"1ste druk"|"393478793"|"denha004koni01"|"1"|"1671"|"maer005"|"Cornelis"||"Maertsz."|"?"|"na 1671"|||"Wervershoof"||"werve001"||||"0"|"https://dbnl.org/tekst/maer005sing01_01"|"https://dbnl.org/nieuws/text.php?id=maer005sing01"|"2012_10 "|"poëzie"| "will028belg00"|"Belgisch museum voor de Nederduitsche tael- en letterkunde en de geschiedenis des vaderlands"||"1837-1846"|"1ste druk"|"394987047"||"1"|"1837"|"will028"|"J.F."||"Willems"|"1793"|"1846"|"11 maart"|"24 juni"|"Boechout"|"Gent"|"boech001"||"gent_001"||"0"|"https://dbnl.org/tekst/will028belg00_01"|||"proza"| "will028belg00"|"Belgisch museum voor de Nederduitsche tael- en letterkunde en de geschiedenis des vaderlands"||"1837-1846"|"1ste druk"|"394987047"||"1"|"1837"|"_bel001"|||"[tijdschrift] Belgisch Museum"|||||||||||"0"|"https://dbnl.org/tekst/will028belg00_01"|||"proza"| +"_ale002alex01"|"Die historie dat leven ende dat regiment des alre grootsten ende machtichsten coninc alexanders die heer was ende prince alle der werelt"||"1477"|"1ste druk"||"berli004staa01"|"1"|"1477"|"_ale002"|"anoniem"||"Die hystorie vanden grooten Coninck Alexander"|||||||||||"0"|"https://dbnl.org/tekst/_ale002alex01_01"|"https://dbnl.org/nieuws/text.php?id=_ale002alex01"|"2020_05 "|"proza"| +"_ale002alex01"|"Die historie dat leven ende dat regiment des alre grootsten ende machtichsten coninc alexanders die heer was ende prince alle der werelt"||"1477"|"1ste druk"||"berli004staa01"|"1"|"1477"|"_ale002"|"anoniem"||"Die hystorie vanden grooten Coninck Alexander"|||||||||||"0"|"https://dbnl.org/tekst/_ale002alex01_01"|"https://dbnl.org/nieuws/text.php?id=_ale002alex01"|"2020_05 "|"non-fictie"| diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index f09aee2b9..5e9e7972a 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -31,7 +31,7 @@ def test_which_unique(items, uniquenesses): def test_metadata_extraction(dbnl_corpus): corpus = load_corpus('dbnl_metadata') data = index_by_id(corpus.documents()) - assert len(data) == 7 + assert len(data) == 8 item = data['maer005sing01'] assert item['title'] == 'Het singende nachtegaeltje' @@ -166,6 +166,8 @@ def test_append_to_tag(xml, tag, padding, original_output, new_output): 'author_id': 'will028', 'author': 'J.F. Willems', 'periodical': 'Belgisch Museum', + }, { #anonymous author + 'author': 'anoniem [Die hystorie vanden grooten Coninck Alexander]' } ] @@ -173,10 +175,12 @@ def test_dbnl_extraction(dbnl_corpus): corpus = load_corpus(dbnl_corpus) docs = list(corpus.documents()) - assert len(docs) == 3 + 6 # 3 chapters + 6 metadata-only books + assert len(docs) == 3 + 7 # 3 chapters + 7 metadata-only books for actual, expected in zip(docs, expected_docs): # assert that actual is a superset of expected + for key in expected: + assert expected[key] == actual[key] assert expected.items() <= actual.items() diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 88e7338f5..8dacb60a4 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -147,6 +147,12 @@ def between_years(year, start_date, end_date): def format_name(parts): '''Format a person's name''' + + #exception for anonymous authors + if parts[0] == 'anoniem': + work = parts[-1] + return f'anoniem [{work}]' + return ' '.join(filter(None, parts)) LINE_TAG = re.compile('^(p|l|head|row|item)$') From 6d422a7a41dc3698eb78bccf753df928a0e60af3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 14 Jun 2023 13:36:19 +0200 Subject: [PATCH 237/262] standardise language codes before name lookup --- backend/corpora/dbnl/tests/test_dbnl_extraction.py | 13 ++++++++++++- backend/corpora/dbnl/utils.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index 5e9e7972a..f821084d3 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -4,7 +4,7 @@ from addcorpus.load_corpus import load_corpus from addcorpus.extract import XML -from corpora.dbnl.utils import append_to_tag, index_by_id, which_unique +from corpora.dbnl.utils import append_to_tag, index_by_id, which_unique, language_name here = os.path.abspath(os.path.dirname(__file__)) @@ -18,6 +18,17 @@ def dbnl_corpus(settings): } return 'dbnl' +language_name_testcases = [ + ('nl', 'Dutch'), + ('la', 'Latin'), + ('lat', 'Latin'), + ('rus', 'Russian') +] + +@pytest.mark.parametrize(['code', 'name'], language_name_testcases) +def test_language_names(code, name): + assert language_name(code) == name + which_unique_testcases = [ (['_ale002', '_ale002'], [True, False]), (['proza', 'poëzie', 'proza'], [True, True, False]) diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 8dacb60a4..c1a27a002 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -196,7 +196,7 @@ def language_name(code): return None codes = code.split('-') names = set(map( - lambda code: Language.make(language=code).display_name(), + lambda code: Language.make(language=standardize_tag(code)).display_name(), codes )) return ' / '.join(names) From 487410b37064afdaa4b5bd3639658ab73d952422 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 14 Jun 2023 15:07:26 +0200 Subject: [PATCH 238/262] update backend tests for new corpus properties --- backend/addcorpus/tests/test_corpus_views.py | 9 ++++----- backend/tag/tests/tag_mock_corpus.py | 2 ++ backend/wordmodels/tests/mock-corpus/mock_corpus.py | 3 +++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/addcorpus/tests/test_corpus_views.py b/backend/addcorpus/tests/test_corpus_views.py index 578fb8738..354faa3fa 100644 --- a/backend/addcorpus/tests/test_corpus_views.py +++ b/backend/addcorpus/tests/test_corpus_views.py @@ -26,11 +26,10 @@ def test_no_corpus_access(db, client, mock_corpus): response = client.get(f'/api/corpus/documentation/{mock_corpus}/mock-csv-corpus.md') assert response.status_code == 403 -def test_corpus_serialization(client, mock_corpus, mock_corpus_user): - client.force_login(mock_corpus_user) - response = client.get('/api/corpus/') - corpus = response.data[0] - assert corpus['title'] == MockCSVCorpus.title +def test_corpus_serialization(admin_client, mock_corpus): + response = admin_client.get('/api/corpus/') + corpus = next(c for c in response.data if c['title'] == MockCSVCorpus.title) + assert corpus assert corpus['languages'] == ['English'] assert corpus['category'] == 'Books' assert len(corpus['fields']) == 2 diff --git a/backend/tag/tests/tag_mock_corpus.py b/backend/tag/tests/tag_mock_corpus.py index cc5c2bef6..3a92e6b78 100644 --- a/backend/tag/tests/tag_mock_corpus.py +++ b/backend/tag/tests/tag_mock_corpus.py @@ -15,6 +15,8 @@ class TaggingMockCorpus(CSVCorpus): max_date = datetime.datetime(year=2022, month=12, day=31) image = 'nothing.jpeg' data_directory = os.path.join(here, 'csv_example') + languages = ['en'] + category = 'book' fields = [ diff --git a/backend/wordmodels/tests/mock-corpus/mock_corpus.py b/backend/wordmodels/tests/mock-corpus/mock_corpus.py index a85c73eba..caa5eaaac 100644 --- a/backend/wordmodels/tests/mock-corpus/mock_corpus.py +++ b/backend/wordmodels/tests/mock-corpus/mock_corpus.py @@ -19,3 +19,6 @@ class WordmodelsMockCorpus(Corpus): name = 'content', ) ] + languages = ['en'] + category = 'book' + From c24f65331208d9265b56ff776feafcbc6bec71bc Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 14 Jun 2023 15:52:07 +0200 Subject: [PATCH 239/262] use date-picker component in search --- .../date-picker/date-picker.component.html | 3 +-- .../date-picker/date-picker.component.ts | 10 +++++++-- .../src/app/filter/base-filter.component.ts | 2 +- .../src/app/filter/date-filter.component.html | 14 ++++++------ .../src/app/filter/date-filter.component.ts | 22 +++++++++++-------- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html index 0b75ded5e..14744d792 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html +++ b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.html @@ -2,6 +2,5 @@ [minDate]="minDate" [maxDate]="maxDate" [ngModel]="subject?.value || default" (onSelect)="set($event)" - (keydown.enter)="set($event.target.value)" - (onBlur)="set($event.target.value)"> + (keydown.enter)="set($event.target.value)"> diff --git a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts index fe70b66e1..a02688465 100644 --- a/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts +++ b/frontend/src/app/corpus-selection/corpus-filter/date-picker/date-picker.component.ts @@ -21,7 +21,7 @@ export class DatePickerComponent { return this.unit === 'year' ? 'yy' : 'dd-mm-yy'; } - set(value: string|Date) { + formatInput(value: string|Date): Date { let valueAsDate: Date; if (typeof(value) == 'string') { const format = this.unit === 'year' ? 'YYYY' : 'DD-MM-YYYY'; @@ -33,7 +33,13 @@ export class DatePickerComponent { valueAsDate = value; } - this.subject.next(valueAsDate); + return valueAsDate; + } + + set(value: string|Date) { + const valueAsDate = this.formatInput(value); + const checkedValue = _.min([_.max([valueAsDate, this.minDate]), this.maxDate]); + this.subject.next(checkedValue); } } diff --git a/frontend/src/app/filter/base-filter.component.ts b/frontend/src/app/filter/base-filter.component.ts index 0bd547889..50ecca998 100644 --- a/frontend/src/app/filter/base-filter.component.ts +++ b/frontend/src/app/filter/base-filter.component.ts @@ -24,7 +24,7 @@ export abstract class BaseFilterComponent implements OnChanges { } ngOnChanges(changes: SimpleChanges): void { - if (changes.filtter) { + if (changes.filter) { this.onFilterSet(this.filter); } diff --git a/frontend/src/app/filter/date-filter.component.html b/frontend/src/app/filter/date-filter.component.html index 708351ec9..865a52224 100644 --- a/frontend/src/app/filter/date-filter.component.html +++ b/frontend/src/app/filter/date-filter.component.html @@ -1,8 +1,8 @@ -
- - +
+ +
diff --git a/frontend/src/app/filter/date-filter.component.ts b/frontend/src/app/filter/date-filter.component.ts index 9c88ee5f1..33c757a7d 100644 --- a/frontend/src/app/filter/date-filter.component.ts +++ b/frontend/src/app/filter/date-filter.component.ts @@ -3,6 +3,8 @@ import * as _ from 'lodash'; import { DateFilterData, DateFilter } from '../models'; import { BaseFilterComponent } from './base-filter.component'; +import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; @Component({ selector: 'ia-date-filter', @@ -12,19 +14,21 @@ import { BaseFilterComponent } from './base-filter.component'; export class DateFilterComponent extends BaseFilterComponent { public minDate: Date; public maxDate: Date; - public minYear: number; - public maxYear: number; + + public selectedMinDate: BehaviorSubject; + public selectedMaxDate: BehaviorSubject; onFilterSet(filter: DateFilter): void { this.minDate = filter.defaultData.min; this.maxDate = filter.defaultData.max; - this.minYear = this.minDate.getFullYear(); - this.maxYear = this.maxDate.getFullYear(); - } - updateProperty(property: 'min'|'max', date: Date) { - const value = _.merge(_.clone(this.data), {[property]: date}); - this.update(value); - } + this.selectedMinDate = new BehaviorSubject(this.minDate); + this.selectedMaxDate = new BehaviorSubject(this.maxDate); + combineLatest([this.selectedMinDate, this.selectedMaxDate]).pipe( + tap(value => console.log(value)) + ).subscribe(([min, max]) => + this.update({min, max}) + ); + } } From e7e872abad48088a985e92e602b0ffcd802266e2 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 14 Jun 2023 16:10:36 +0200 Subject: [PATCH 240/262] fix to frontend tests --- frontend/src/app/models/query.spec.ts | 2 +- frontend/src/mock-data/corpus.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index f16c8cc8d..390f81b3f 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -23,7 +23,7 @@ const corpus: Corpus = { ], languages: ['English'], category: 'Tests', -}; +} as Corpus; describe('QueryModel', () => { let query: QueryModel; diff --git a/frontend/src/mock-data/corpus.ts b/frontend/src/mock-data/corpus.ts index 17c549c01..d20579a00 100644 --- a/frontend/src/mock-data/corpus.ts +++ b/frontend/src/mock-data/corpus.ts @@ -190,7 +190,7 @@ export const mockCorpus3: Corpus = { sortField: mockField3, sortDirection: 'asc' } -}; +} as Corpus; export class CorpusServiceMock { private currentCorpusSubject = new BehaviorSubject(mockCorpus); From 22a8ae9664ea53590653998dc0f363b59ed681e0 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 15 Jun 2023 12:06:45 +0200 Subject: [PATCH 241/262] fix duplicate periodical names in case of multiple genres --- backend/corpora/dbnl/tests/data/titels_pd.csv | 4 ++++ backend/corpora/dbnl/tests/test_dbnl_extraction.py | 7 +++++-- backend/corpora/dbnl/utils.py | 7 +++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/corpora/dbnl/tests/data/titels_pd.csv b/backend/corpora/dbnl/tests/data/titels_pd.csv index 9b2239e30..035a37c96 100644 --- a/backend/corpora/dbnl/tests/data/titels_pd.csv +++ b/backend/corpora/dbnl/tests/data/titels_pd.csv @@ -12,3 +12,7 @@ ti_id|titel|vols|jaar|druk|ppn_o|bibliotheek|categorie|_jaar|pers_id|voornaam|vo "will028belg00"|"Belgisch museum voor de Nederduitsche tael- en letterkunde en de geschiedenis des vaderlands"||"1837-1846"|"1ste druk"|"394987047"||"1"|"1837"|"_bel001"|||"[tijdschrift] Belgisch Museum"|||||||||||"0"|"https://dbnl.org/tekst/will028belg00_01"|||"proza"| "_ale002alex01"|"Die historie dat leven ende dat regiment des alre grootsten ende machtichsten coninc alexanders die heer was ende prince alle der werelt"||"1477"|"1ste druk"||"berli004staa01"|"1"|"1477"|"_ale002"|"anoniem"||"Die hystorie vanden grooten Coninck Alexander"|||||||||||"0"|"https://dbnl.org/tekst/_ale002alex01_01"|"https://dbnl.org/nieuws/text.php?id=_ale002alex01"|"2020_05 "|"proza"| "_ale002alex01"|"Die historie dat leven ende dat regiment des alre grootsten ende machtichsten coninc alexanders die heer was ende prince alle der werelt"||"1477"|"1ste druk"||"berli004staa01"|"1"|"1477"|"_ale002"|"anoniem"||"Die hystorie vanden grooten Coninck Alexander"|||||||||||"0"|"https://dbnl.org/tekst/_ale002alex01_01"|"https://dbnl.org/nieuws/text.php?id=_ale002alex01"|"2020_05 "|"non-fictie"| +"_gid001184801"|"De Gids. Jaargang 12"||"1848"|"1ste druk"||"leide001univ01"|"1"|"1848"|"_gid001"|||"[tijdschrift] Gids, De"|||||||||||"0"|"https://dbnl.org/tekst/_gid001184801_01"|"https://dbnl.org/nieuws/text.php?id=_gid001184801"|"2008_03 "|"proza"| +"_gid001184801"|"De Gids. Jaargang 12"||"1848"|"1ste druk"||"leide001univ01"|"1"|"1848"|"_gid001"|||"[tijdschrift] Gids, De"|||||||||||"0"|"https://dbnl.org/tekst/_gid001184801_01"|"https://dbnl.org/nieuws/text.php?id=_gid001184801"|"2008_03 "|"poëzie"| +"_gid001184801"|"De Gids. Jaargang 12"||"1848"|"1ste druk"||"leide001univ01"|"1"|"1848"|"_gid001"|||"[tijdschrift] Gids, De"|||||||||||"0"|"https://dbnl.org/tekst/_gid001184801_01"|"https://dbnl.org/nieuws/text.php?id=_gid001184801"|"2008_03 "|"sec - letterkunde"| +"_gid001184801"|"De Gids. Jaargang 12"||"1848"|"1ste druk"||"leide001univ01"|"1"|"1848"|"_gid001"|||"[tijdschrift] Gids, De"|||||||||||"0"|"https://dbnl.org/tekst/_gid001184801_01"|"https://dbnl.org/nieuws/text.php?id=_gid001184801"|"2008_03 "|"sec - taalkunde"| diff --git a/backend/corpora/dbnl/tests/test_dbnl_extraction.py b/backend/corpora/dbnl/tests/test_dbnl_extraction.py index f821084d3..33a55981f 100644 --- a/backend/corpora/dbnl/tests/test_dbnl_extraction.py +++ b/backend/corpora/dbnl/tests/test_dbnl_extraction.py @@ -42,7 +42,7 @@ def test_which_unique(items, uniquenesses): def test_metadata_extraction(dbnl_corpus): corpus = load_corpus('dbnl_metadata') data = index_by_id(corpus.documents()) - assert len(data) == 8 + assert len(data) == 9 item = data['maer005sing01'] assert item['title'] == 'Het singende nachtegaeltje' @@ -179,6 +179,9 @@ def test_append_to_tag(xml, tag, padding, original_output, new_output): 'periodical': 'Belgisch Museum', }, { #anonymous author 'author': 'anoniem [Die hystorie vanden grooten Coninck Alexander]' + }, { # periodical with multiple genres + 'author': None, + 'periodical': 'Gids, De' } ] @@ -186,7 +189,7 @@ def test_dbnl_extraction(dbnl_corpus): corpus = load_corpus(dbnl_corpus) docs = list(corpus.documents()) - assert len(docs) == 3 + 7 # 3 chapters + 7 metadata-only books + assert len(docs) == 3 + 8 # 3 chapters + 7 metadata-only books for actual, expected in zip(docs, expected_docs): # assert that actual is a superset of expected diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index c1a27a002..1d03b202d 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -18,11 +18,14 @@ def index_by_id(data): def is_periodical(name): return name.startswith(PERIODIAL_PREFIX) +def sorted_and_unique(items): + return list(sorted(set(items))) + def get_periodical(names): periodicals = list(filter(is_periodical, names)) format = lambda name: name[len(PERIODIAL_PREFIX):].strip() if periodicals: - return ', '.join(map(format, periodicals)) + return ', '.join(sorted_and_unique(map(format, periodicals))) def which_are_people(names): ''' @@ -199,4 +202,4 @@ def language_name(code): lambda code: Language.make(language=standardize_tag(code)).display_name(), codes )) - return ' / '.join(names) + return ', '.join(names) From c05348b2ebe1b3ff95e8a958a4de42cc831e2597 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Thu, 15 Jun 2023 12:10:17 +0200 Subject: [PATCH 242/262] fix variable order in languages --- backend/corpora/dbnl/dbnl.py | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/backend/corpora/dbnl/dbnl.py b/backend/corpora/dbnl/dbnl.py index 394a341ec..5ea9c1f9a 100644 --- a/backend/corpora/dbnl/dbnl.py +++ b/backend/corpora/dbnl/dbnl.py @@ -254,24 +254,27 @@ def _xml_files(self): # this extractor is similar to language_code below, # but designed to accept multiple values in case of uncertainty extractor=Pass( - Backup( - XML( # get the language on chapter-level if available - attribute='lang', - transform=lambda value: [value] if value else None, - ), - XML( # look for section-level codes - {'name': 'div', 'attrs': {'type': 'section'}}, - attribute='lang', - multiple=True, - ), - XML( # look in the top-level metadata - 'language', - toplevel=True, - recursive=True, - multiple=True, - attribute='id' + Pass( + Backup( + XML( # get the language on chapter-level if available + attribute='lang', + transform=lambda value: [value] if value else None, + ), + XML( # look for section-level codes + {'name': 'div', 'attrs': {'type': 'section'}}, + attribute='lang', + multiple=True, + ), + XML( # look in the top-level metadata + 'language', + toplevel=True, + recursive=True, + multiple=True, + attribute='id' + ), + transform = lambda codes: map(utils.language_name, codes) if codes else None, ), - transform = lambda codes: map(utils.language_name, codes) if codes else None, + transform=utils.sorted_and_unique, ), transform=utils.join_values, ), From 556c2757c6fc3ce32ff0e911b8b56776a5a2bf19 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Fri, 16 Jun 2023 11:36:12 +0200 Subject: [PATCH 243/262] handle None input for sorted_and_unique function --- backend/corpora/dbnl/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/corpora/dbnl/utils.py b/backend/corpora/dbnl/utils.py index 1d03b202d..7ffeab826 100644 --- a/backend/corpora/dbnl/utils.py +++ b/backend/corpora/dbnl/utils.py @@ -19,7 +19,8 @@ def is_periodical(name): return name.startswith(PERIODIAL_PREFIX) def sorted_and_unique(items): - return list(sorted(set(items))) + if items: + return list(sorted(set(items))) def get_periodical(names): periodicals = list(filter(is_periodical, names)) From aa747ed2d1d424ec87278481e1fed9462b448f81 Mon Sep 17 00:00:00 2001 From: Luka van der Plas <43678097+lukavdplas@users.noreply.github.com> Date: Mon, 19 Jun 2023 17:40:10 +0200 Subject: [PATCH 244/262] correct Defining-corpus-fields.md --- documentation/Defining-corpus-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/Defining-corpus-fields.md b/documentation/Defining-corpus-fields.md index 4e1b91cef..b53d8dab2 100644 --- a/documentation/Defining-corpus-fields.md +++ b/documentation/Defining-corpus-fields.md @@ -9,7 +9,7 @@ Various classes are defined in `backend/addcorpus/extract.py`. - The extractors `XML`, `HTML` and `CSV` are intended to extract values from the document type of your corpus. Naturally, `XML` is only available for `XMLCorpus`, et cetera. All other extractors are available for all corpora. - The `Metadata` extractor is used to collect any information that you passed on during file discovery, such as information based on the file path. - The `Constant` extractor can be used to define a constant value. -- The `Index` extractor gives you the index of that document within the file. +- The `Order` extractor gives you the index of that document within the file. - The `Choice` and `Combined`, `Backup`, and `Pass` extractors can be used to combine multiple extractors. A field can have the property `required = True`, which means the document will not be added to the index if the extracted value for this field is falsy. From a951436fe9933ecf3e06cb4b06b4558a6cb77236 Mon Sep 17 00:00:00 2001 From: Luka van der Plas <43678097+lukavdplas@users.noreply.github.com> Date: Mon, 19 Jun 2023 17:52:36 +0200 Subject: [PATCH 245/262] Update Migration-from-flask.md Updates based on production server migration --- documentation/Migration-from-flask.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/documentation/Migration-from-flask.md b/documentation/Migration-from-flask.md index a4f60fda5..2f4be1d4a 100644 --- a/documentation/Migration-from-flask.md +++ b/documentation/Migration-from-flask.md @@ -79,19 +79,7 @@ Regarding the directory: - In production, the location of the flask migration is stored in the django settings. Use `directory = settings.FLASK_MIGRATION_DATA` - If `directory` does not exist or does not contain relevant files, the script will not import anything. -The script expects to run on an **empty** database, as it will also copy object IDs. This means that if the script fails halfway through, you will need to reset the database before you can re-attempt. You can use the following script in the shell to do so: - -``` -from django.contrib.auth.models import Group -from users.models import CustomUser -from addcorpus.models import Corpus - -Group.objects.all().delete() -CustomUser.objects.all().delete() -Corpus.objects.all().delete() -``` - -Objects in other tables (such as the search history) will be deleted through cascade. +The script expects to run on an **empty** database, as it will also copy object IDs. This means that if the script fails halfway through, you will need to reset the database before you can re-attempt. You can do this from the command line with `yarn django flush`. ### Update object IDs @@ -108,10 +96,12 @@ python manage.py sqlsequencereset download | python manage.py dbshell In `backend/ianalyzer`, make a file `settings_local.py`. Transfer relevant local settings you had configured in your `config.py` file for Flask. +Note that the new `settings_local` does not need all the information you had provided in`config`. For a development environment, is is probably sufficient to simply specify the `CORPORA`, and the locations of corpus source data and word model files. + ## Transfer downloads In the flask backend, the default storage location for CSV files was `/backend/api/csv_files/`. In a development environment, the new default location is `/backend/download/csv_files/`. (This can be configured in settings.) You will have to move the contents of your CSV directory here if you want to keep your download history. -For a production environment, the csv files need to be moved from the old flask server to the new django server. Check the deployment settings for the new location of the downloads. (This should be outside of the repository.) +For a production environment, the csv files need to be moved from the old flask server to the new django server, if you are also moving servers. Check the deployment settings for the new location of the downloads. (This should be outside of the repository.) From 2de659bce2f4cd7109f387539c1cfad17e30f197 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 21 Jun 2023 08:54:22 +0200 Subject: [PATCH 246/262] remove legacy config.py --- backend/.gitignore | 5 + backend/ianalyzer/config.py | 205 ------------------------------------ 2 files changed, 5 insertions(+), 205 deletions(-) delete mode 100644 backend/ianalyzer/config.py diff --git a/backend/.gitignore b/backend/.gitignore index ef5f1e392..56da2bfd0 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -37,9 +37,14 @@ flask_sql_data/ # Local settings file ianalyzer/settings_local.py +# legacy config +ianalyzer/config.py + # csv downloads download/csv_files/ # word models corpora/*/wm/* !corpora/*/wm/documentation.md + + diff --git a/backend/ianalyzer/config.py b/backend/ianalyzer/config.py deleted file mode 100644 index d879b996d..000000000 --- a/backend/ianalyzer/config.py +++ /dev/null @@ -1,205 +0,0 @@ -''' -Configuration. -''' - -import logging -import csv -from os.path import expanduser, realpath, join, dirname, relpath -from datetime import datetime, timedelta - - -LOG_LEVEL = logging.INFO - -# Flask -DEBUG = True -TESTING = False -SECRET_KEY = '0987654321' -SECURITY_PASSWORD_SALT = '42istheanswertothelastofallquestions' -SECURITY_RECOVERABLE = True - -MAIL_SERVER = 'localhost' -MAIL_PORT = 25 -MAIL_USE_TLS = False -MAIL_USE_SSL = False -MAIL_USERNAME = '' -MAIL_PASSWORD = '' -MAIL_FROM_ADRESS = 'example@dhlab.nl' -MAIL_REGISTRATION_SUBJECT_LINE = 'Thank you for signing up at I-analyzer' -CSV_FILES_PATH = '/Users/janss089/git/ianalyzer/backend/api/csv_files' -BASE_URL = 'http://localhost:4200' -LOGO_LINK = 'http://dhstatic.hum.uu.nl/logo-lab/png/dighum-logo.png' - -# SQLAlchemy -SQLALCHEMY_DATABASE_URI = 'mysql://ianalyzer@localhost/ianalyzer' -SQLALCHEMY_TRACK_MODIFICATIONS = True - -ES_SEARCH_TIMEOUT = '30s' - -GOODREADS_DATA = '/Users/janss089/Desktop/Goodreads-2022' # only needed for indexing -GOODREADS_ES_INDEX = 'ianalyzer-goodreads' - -PEACEPORTAL_TOL_ES_INDEX = 'peaceportal-tol' -PEACEPORTAL_TOL_DATA = '/Users/janss089/DATA/peaceportal/TOL' - -PP_IRELAND_DATA = "some-path" -PP_IRELAND_INDEX = 'parliament-ireland' - -DUTCHNEWSPAPERS_ALL_DATA = '/Users/janss089/Desktop/DDD_000010100' -DUTCHNEWSPAPERS_ALL_ES_INDEX = 'ianalyzer-dutchnewspapers-all' - -# the corpora variable provides the file path of the corpus definition -# needs to be a full (not relative) file path -CORPORA = { - 'times': '/Users/janss089/git/ianalyzer/backend/corpora/times/times.py', - 'dutchnewspapers-public': '/Users/janss089/git/ianalyzer/backend/corpora/dutchnewspapers/dutchnewspapers_public.py', - 'dutchnewspapers-all': '/Users/janss089/git/ianalyzer/backend/corpora/dutchnewspapers/dutchnewspapers_all.py', - 'dutchannualreports': '/Users/janss089/git/ianalyzer/backend/corpora/dutchannualreports/dutchannualreports.py', - 'goodreads': '/Users/janss089/git/ianalyzer/backend/corpora/goodreads/goodreads.py', - 'troonredes': '/Users/janss089/git/ianalyzer/backend/corpora/troonredes/troonredes.py', - 'guardianobserver': '/Users/janss089/git/ianalyzer/backend/corpora/guardianobserver/guardianobserver.py', - 'periodicals': '/Users/janss089/git/ianalyzer/backend/corpora/periodicals/periodicals.py', - 'jewishinscriptions': '/Users/janss089/git/ianalyzer/backend/corpora/jewishinscriptions/jewishinscriptions.py', - 'ecco': '/Users/janss089/git/ianalyzer/backend/corpora/ecco/ecco.py', - 'parliament-netherlands': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/netherlands.py', - # 'parliament-norway': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/norway.py', - 'parliament-uk': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/uk.py', - # 'parliament-uk-recent': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/uk_recent.py', - # 'parliament-netherlands-recent': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/netherlands_recent.py' - # 'parliament': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/parliament.py' - 'parliament-france': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/france.py', - 'parliament-germany-new': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/germany-new.py', - 'parliament-ireland': '/Users/janss089/git/ianalyzer/backend/corpora/parliament/ireland.py', - # 'dutchnewspapers-public': - # 'fiji': '/Users/janss089/git/ianalyzer/backend/corpora/peaceportal/FIJI/fiji.py', - # 'peaceportal': '/Users/janss089/git/ianalyzer/backend/corpora/peaceportal/peaceportal.py' - # 'tol': '/Users/janss089/git/ianalyzer/backend/corpora/peaceportal/tol.py' -} - -SERVERS = { - # Default ElasticSearch server - 'es8': { - 'host': 'localhost', - 'port': 9200, - 'api_id': 'AEIMRIEBauFysRDbdKAZ', - 'api_key': 'x5Fq2w5TQsGa2lbLBNVAgA', - 'certs_location': '/Applications/elasticsearch-8.0.0/config/certs/http_ca.crt', - 'chunk_size': 900, # Maximum number of documents sent during ES bulk operation - 'max_chunk_bytes': 1*1024*1024, # Maximum size of ES chunk during bulk operation - 'bulk_timeout': '60s', # Timeout of ES bulk operation - 'overview_query_size': 20, # Number of results to appear in the overview query - 'scroll_timeout': '3m', # Time before scroll results time out - 'scroll_page_size': 5000, # Number of results per scroll page - }, - 'default': { - 'host': 'localhost', - 'port': 9200, - 'chunk_size': 900, # Maximum number of documents sent during ES bulk operation - 'max_chunk_bytes': 1*1024*1024, # Maximum size of ES chunk during bulk operation - 'bulk_timeout': '60s', # Timeout of ES bulk operation - 'overview_query_size': 20, # Number of results to appear in the overview query - 'scroll_timeout': '3m', # Time before scroll results time out - 'scroll_page_size': 5000, - } -} - -SSL_CERT = '' - -# Index configurations -TIMES_ES_INDEX = 'times' -TIMES_ES_DOCTYPE = 'article' -TIMES_DATA = '/Users/janss089/DATA/times_test' - -DUTCHANNUALREPORTS_ES_INDEX = 'dutchannualreports' -DUTCHANNUALREPORTS_DATA = '/Users/janss089/DATA/NewDutchBanking' -DUTCHANNUALREPORTS_WM = '/Users/janss089/git/wordmodels/dutchannual' - -# DUTCHNEWSPAPERS_ALL_ES_INDEX = 'dutchnewspapers-all' -# DUTCHNEWSPAPERS_ALL_DATA = '/Users/janss089/DATA/kranten-delpher' - -DUTCHNEWSPAPERS_ES_INDEX = 'dutchnewspapers-public' -DUTCHNEWSPAPERS_ES_DOCTYPE = 'article' -DUTCHNEWSPAPERS_DATA = '/Users/janss089/DATA/kranten_test' -DUTCHNEWSPAPERS_TITLE = 'Dutch Newspapers' -DUTCHNEWSPAPERS_DESCRIPTION = 'Freely available part of the Delpher corpus, 1618-1876' - -GO_ES_INDEX = 'guardianobserver' -GO_ES_DOCTYPE = 'article' -GO_DATA = '/Users/janss089/DATA/guardian' - -TROONREDES_DATA = '/Users/janss089/DATA/troonredes' -TROONREDES_ES_INDEX = 'troonredes' -TROONREDES_ES_DOCTYPE = 'speech' - -PERIODICALS_DATA = '/Users/janss089/DATA/19thCenturyPeriodicals' -PERIODICALS_ES_INDEX = 'periodicals' -PERIODICALS_ES_DOCTYPE = 'article' - -JEWISH_INSCRIPTIONS_DATA = '/Users/janss089/DATA/jewish-inscriptions' -JEWISH_INSCRIPTIONS_ES_INDEX = 'jewishinscriptions' -JEWISH_INSCRIPTIONS_ES_DOCTYPE = 'epitaph' - -ECCO_DATA = '/Users/janss089/DATA/ecco' -ECCO_ES_INDEX = 'ecco' -ECCO_ES_DOCTYPE = 'page' - -PEACEPORTAL_FIJI_DATA = '/Users/janss089/DATA/peaceportal/FIJI' -PEACEPORTAL_FIJI_ES_INDEX = 'peaceportal-fiji' -PEACEPORTAL_ALIAS = 'peaceportal' - -PP_ALIAS = 'parliament' -PP_UK_DATA = '/Users/janss089/DATA/PeopleParliament/' -PP_UK_RECENT_DATA = '/Users/janss089/Desktop/RecentNL/ParlaMint-NL/ParlaMint-NL.TEI' -PP_UK_INDEX = 'parliament-uk' - -PP_NO_INDEX = 'parliament-norway' -PP_NO_DATA = '/Users/janss089/DATA/PeopleParliament/Norway' - -PP_NL_INDEX = 'parliament-netherlands' -PP_NL_DATA = '/Users/janss089/Desktop/uncompressed/d/nl/proc' -# PP_NL_RECENT_DATA = '/Users/janss089/Desktop/ParlaMint-NL/ParlaMint-NL.TEI' - - -PP_FR_INDEX = 'parliament-france' -# PP_FR_DATA = '/Users/janss089/Desktop/test' -PP_FR_DATA = '/Volumes/data/GW/CDH/DHLab/PeopleandParliament/ExampleData/France/' -PP_FR_WM = '/Users/janss089/Desktop/wm-france/france' - -PP_GERMANY_NEW_DATA = '/Volumes/data/GW/CDH/DHLab/PeopleandParliament/ExampleData/Germany-tiny/' -PP_GERMANY_NEW_INDEX = 'parliament-germany-new' - -PP_UK_WM = '/Users/janss089/Desktop/uk-lem-sep' - -# TROONREDES_WM = '/Users/janss089/git/wordmodels/troonredes' - -PP_ES_SETTINGS = { - "analysis": { - "analyzer": { - "clean": { - "tokenizer": "standard", - "char_filter": ["number_filter"], - "filter": ["lowercase", "stopwords"] - }, - "stemmed": { - "tokenizer": "standard", - "char_filter": ["number_filter"], - "filter": ["lowercase", "stopwords", "stemmer"] - } - }, - "char_filter":{ - "number_filter":{ - "type":"pattern_replace", - "pattern":"\\d+", - "replacement":"" - } - } - } -} - -# SAML -SAML_FOLDER = "/Users/janss089/git/ianalyzer/saml" -SAML_SOLISID_KEY = "uuShortID" -SAML_MAIL_KEY = "mail" - - -CORPUS_SERVER_NAMES = { -} From a6d1c8d816ed04cb7c5fe63261d2e65e968ccf12 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Wed, 21 Jun 2023 14:10:02 +0200 Subject: [PATCH 247/262] Add CITATION.cff --- CITATION.cff | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..d2bf48475 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,36 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: I-Analyzer +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - name: Research Software Lab + email: digitalhumanties@uu.nl + affiliation: 'Centre for Digital Humanities, Utrecht University' +repository-code: 'https://github.com/UUDigitalHumanitieslab/I-analyzer' +url: 'https://ianalyzer.hum.uu.nl' +abstract: >- + I-analyzer is a tool for exploring corpora (large + collections of texts). You can use I-analyzer to find + relevant documents, or to make visualisations to + understand broader trends in the corpus. The interface is + designed to be accessible for users of all skill levels. + + I-analyzer is primarily intended for academic research and + higher education. We focus on data that is relevant for + the humanities, but we are open to datasets that are + relevant for other fields. +keywords: + - text-mining + - corpus research + - data visualization + - elasticsearch + - natural language processing +license: MIT +commit: 96b9585 +version: 4.0.2 +date-released: '2023-06-21' From b75ad130e975e7c820d9a2f62778d4e54c1d1e94 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Wed, 21 Jun 2023 14:48:19 +0200 Subject: [PATCH 248/262] Add Zenodo badge & update CITATION.cff --- CITATION.cff | 14 +++++++++----- README.md | 2 ++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index d2bf48475..83bfbb734 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,9 +8,13 @@ message: >- metadata from this file. type: software authors: - - name: Research Software Lab - email: digitalhumanties@uu.nl - affiliation: 'Centre for Digital Humanities, Utrecht University' + - name: 'Research Software Lab, Centre for Digital Humanities, Utrecht University' + website: 'https://cdh.uu.nl/centre-for-digital-humanities/research-software-lab/' + city: Utrecht + country: NL +identifiers: + - type: doi + value: 10.5281/zenodo.8064133 repository-code: 'https://github.com/UUDigitalHumanitieslab/I-analyzer' url: 'https://ianalyzer.hum.uu.nl' abstract: >- @@ -31,6 +35,6 @@ keywords: - elasticsearch - natural language processing license: MIT -commit: 96b9585 -version: 4.0.2 +commit: fb80497 +version: 4.0.3 date-released: '2023-06-21' diff --git a/README.md b/README.md index 665d5ba64..63f0b9900 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8064133.svg)](https://doi.org/10.5281/zenodo.8064133) [![Actions Status](https://github.com/UUDigitalHumanitiesLab/I-analyzer/workflows/Unit%20tests/badge.svg)](https://github.com/UUDigitalHumanitiesLab/I-analyzer/actions) + # I-analyzer The text mining tool that obviates all others. From edb6502aee554e7bd9fcd771df1e518be3dd5152 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 6 Jun 2023 10:51:03 +0200 Subject: [PATCH 249/262] update logo in footer --- frontend/src/app/footer/footer.component.html | 14 +++++++------- ..._logo_EN_def_UU_CDH_logo_EN_yellowwhite.jpg | Bin 0 -> 186398 bytes frontend/src/assets/dhlab.png | Bin 255858 -> 0 bytes frontend/src/assets/uu-dhlab.png | Bin 21422 -> 0 bytes 4 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 frontend/src/assets/UU-CDH_logo_EN_def_UU_CDH_logo_EN_yellowwhite.jpg delete mode 100644 frontend/src/assets/dhlab.png delete mode 100644 frontend/src/assets/uu-dhlab.png diff --git a/frontend/src/app/footer/footer.component.html b/frontend/src/app/footer/footer.component.html index d44e20643..84fd7a699 100644 --- a/frontend/src/app/footer/footer.component.html +++ b/frontend/src/app/footer/footer.component.html @@ -2,17 +2,17 @@
-

+

With support from -

+

diff --git a/frontend/src/assets/UU-CDH_logo_EN_def_UU_CDH_logo_EN_yellowwhite.jpg b/frontend/src/assets/UU-CDH_logo_EN_def_UU_CDH_logo_EN_yellowwhite.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1115cebbdf301692cbbb2ef6b61e4f6383539c0c GIT binary patch literal 186398 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 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/dhlab.png b/frontend/src/assets/dhlab.png deleted file mode 100644 index 40064be4d568244d1301d672dfcfb4bd12ce1769..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255858 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 YzkEk9|g@88awzvl7t%WEvf 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 QHvj+t07*qoM6N<$f>lQmV*mgE diff --git a/frontend/src/assets/uu-dhlab.png b/frontend/src/assets/uu-dhlab.png deleted file mode 100644 index f81bc9a5b13905671ba6ef21f7c8fed55206614b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21422 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 Sn!>jQXKG}5vGl^PcmD(4#hHu% From faf47543aa390a9cf7dbb16b0cc1ceb78814f0d3 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Tue, 6 Jun 2023 11:04:42 +0200 Subject: [PATCH 250/262] update org name in email --- backend/download/mail.py | 1 + backend/download/templates/download_mail.html | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/download/mail.py b/backend/download/mail.py index 3b9197a7e..5dc840ec1 100644 --- a/backend/download/mail.py +++ b/backend/download/mail.py @@ -32,6 +32,7 @@ def send_csv_email(user_email, username, download_id): 'link_text': 'Download .csv file', 'logo_link': settings.LOGO_LINK, 'url_i_analyzer': settings.BASE_URL, + 'organization': 'the Research Software Lab at the Centre for Digital Humanities (Utrecht University)' } html_message = render_to_string('download_mail.html', context) diff --git a/backend/download/templates/download_mail.html b/backend/download/templates/download_mail.html index 3bad6d45f..6705a4fdf 100644 --- a/backend/download/templates/download_mail.html +++ b/backend/download/templates/download_mail.html @@ -115,7 +115,7 @@

Hello {{ username }},

{{ message }}

{% if login %} -

Login name: {{ username }}

+

Login name: {{ username }}

{% endif %}

{{ prompt }}

@@ -148,7 +148,7 @@
- I-analyzer is a product of Digital Humanities Lab + I-analyzer is a product of {{organization}}
@@ -162,4 +162,4 @@ - \ No newline at end of file + From eb402317ea7233699acae822d1504e476547b058 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 15:27:18 +0200 Subject: [PATCH 251/262] update logo in download email --- backend/download/templates/download_mail.html | 2 +- backend/ianalyzer/common_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/download/templates/download_mail.html b/backend/download/templates/download_mail.html index 6705a4fdf..2dd2c8f96 100644 --- a/backend/download/templates/download_mail.html +++ b/backend/download/templates/download_mail.html @@ -103,7 +103,7 @@ -
+
diff --git a/backend/ianalyzer/common_settings.py b/backend/ianalyzer/common_settings.py index c758d913f..986a11b91 100644 --- a/backend/ianalyzer/common_settings.py +++ b/backend/ianalyzer/common_settings.py @@ -130,4 +130,4 @@ 'REGISTER_SERIALIZER': 'users.serializers.CustomRegisterSerializer', } -LOGO_LINK = 'http://dhstatic.hum.uu.nl/logo-lab/png/dighum-logo.png' +LOGO_LINK = 'https://dhstatic.hum.uu.nl/logo-cdh/png/UU_CDH_logo_EN_whiteFC.png' From b370444b77896ab5e3ce94236994071e646ae10a Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 15:37:22 +0200 Subject: [PATCH 252/262] add documentation for troonredes --- backend/corpora/troonredes/description/troonredes.md | 5 +++++ backend/corpora/troonredes/troonredes.py | 1 + 2 files changed, 6 insertions(+) create mode 100644 backend/corpora/troonredes/description/troonredes.md diff --git a/backend/corpora/troonredes/description/troonredes.md b/backend/corpora/troonredes/description/troonredes.md new file mode 100644 index 000000000..fe1f39154 --- /dev/null +++ b/backend/corpora/troonredes/description/troonredes.md @@ -0,0 +1,5 @@ +Troonredes (throne speeches) are the speeches from the throne that formally mark the opening of the parliamentary year, known in the Netherlands as “Prinsjesdag”, taking place on the third Tuesday of September each year. During a joint session of the Dutch senate and the house of representatives, the queen or king reads a speech that has been prepared by the Dutch government which outlines the state of affairs and plans for the coming parliamentary year. This corpus contains the “troonredes” from 1814 until 2018, as well as some inaugural addresses, which are speeches given during coronation. + +Missing years: in 1940-1944 no speech was written. + +The transcripts are provided by [troonredes.nl](troonredes.nl). diff --git a/backend/corpora/troonredes/troonredes.py b/backend/corpora/troonredes/troonredes.py index d00ec238e..67c52b860 100644 --- a/backend/corpora/troonredes/troonredes.py +++ b/backend/corpora/troonredes/troonredes.py @@ -37,6 +37,7 @@ class Troonredes(XMLCorpus): word_model_path = getattr(settings, 'TROONREDES_WM', None) languages = ['nl'] category = 'oration' + description_page = 'troonredes.md' tag_toplevel = 'doc' tag_entry = 'entry' From e1ba919ac41b7b2c9639ceb3a7b1067904bf6518 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 15:40:36 +0200 Subject: [PATCH 253/262] add description for rechtspraak --- backend/corpora/rechtspraak/description/rechtspraak.md | 3 +++ backend/corpora/rechtspraak/rechtspraak.py | 1 + 2 files changed, 4 insertions(+) create mode 100644 backend/corpora/rechtspraak/description/rechtspraak.md diff --git a/backend/corpora/rechtspraak/description/rechtspraak.md b/backend/corpora/rechtspraak/description/rechtspraak.md new file mode 100644 index 000000000..3840b7ea2 --- /dev/null +++ b/backend/corpora/rechtspraak/description/rechtspraak.md @@ -0,0 +1,3 @@ +Court rulings published to [uitspraken.rechtspraak.nl](uitspraken.rechtspraak.nl). This corpus contains a portion of all court rulings in the Netherlands; rechtspraak.nl contains [detailed information about the selection criteria](https://www.rechtspraak.nl/Uitspraken/Paginas/Selectiecriteria.aspx). Rulings are anonimised. + +Be aware that while the rulings on rechtspraak.nl are updated regularly, the rulings on I-analyzer are not, and may be out of date. (You can see the latest available publication date in the corpus.) We are working on this! diff --git a/backend/corpora/rechtspraak/rechtspraak.py b/backend/corpora/rechtspraak/rechtspraak.py index 7a88edc8f..37762905e 100644 --- a/backend/corpora/rechtspraak/rechtspraak.py +++ b/backend/corpora/rechtspraak/rechtspraak.py @@ -36,6 +36,7 @@ class Rechtspraak(XMLCorpus): data_directory = settings.RECHTSPRAAK_DATA es_index = getattr(settings, 'RECHTSPRAAK_ES_INDEX', 'rechtspraak') image = 'rechtszaal.jpeg' + description_page = 'rechtspraak.md' toplevel_zip_file = 'OpenDataUitspraken.zip' languages = ['nl'] category = 'ruling' From bffa76745847332065323d8b68dfa54c222719f4 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 16:17:21 +0200 Subject: [PATCH 254/262] add keyboard control and aria fluff to tabs --- frontend/src/app/search/search.component.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 192b4c4ad..afdb2cf16 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -56,13 +56,15 @@
- + - +
From cee7dd32420dc40e26573201afb0299a8a28e329 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 16:19:09 +0200 Subject: [PATCH 255/262] remove console.log --- frontend/src/app/filter/date-filter.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/app/filter/date-filter.component.ts b/frontend/src/app/filter/date-filter.component.ts index 33c757a7d..857d81669 100644 --- a/frontend/src/app/filter/date-filter.component.ts +++ b/frontend/src/app/filter/date-filter.component.ts @@ -25,9 +25,7 @@ export class DateFilterComponent extends BaseFilterComponent { this.selectedMinDate = new BehaviorSubject(this.minDate); this.selectedMaxDate = new BehaviorSubject(this.maxDate); - combineLatest([this.selectedMinDate, this.selectedMaxDate]).pipe( - tap(value => console.log(value)) - ).subscribe(([min, max]) => + combineLatest([this.selectedMinDate, this.selectedMaxDate]).subscribe(([min, max]) => this.update({min, max}) ); } From 6b6df34b88f326dc93c37c9a74311266f5299ff9 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 17:28:45 +0200 Subject: [PATCH 256/262] fix error when switching tabs back and forth --- .../barchart/barchart.directive.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts index b8c680f63..9d071c2c8 100644 --- a/frontend/src/app/visualization/barchart/barchart.directive.ts +++ b/frontend/src/app/visualization/barchart/barchart.directive.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/member-ordering */ -import { Directive, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Directive, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import * as _ from 'lodash'; @@ -10,15 +10,16 @@ import { BarchartSeries, AggregateQueryFeedback, TimelineDataPoint, HistogramDataPoint, TermFrequencyResult, ChartParameters } from '../../models'; import Zoom from 'chartjs-plugin-zoom'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { selectColor } from '../../utils/select-color'; import { VisualizationService } from '../../services/visualization.service'; import { findByName, showLoading } from '../../utils/utils'; +import { takeUntil } from 'rxjs/operators'; const hintSeenSessionStorageKey = 'hasSeenTimelineZoomingHint'; const hintHidingMinDelay = 500; // milliseconds const hintHidingDebounceTime = 1000; // milliseconds - +const barchartID = 'barchart'; @Directive({ selector: 'ia-barchart', @@ -28,7 +29,7 @@ const hintHidingDebounceTime = 1000; // milliseconds * histogram and timeline components. It does not function as a stand-alone component. */ export abstract class BarchartDirective - implements OnChanges, OnInit { + implements OnChanges, OnInit, OnDestroy { public showHint: boolean; // rawData: a list of series @@ -79,6 +80,8 @@ export abstract class BarchartDirective @Output() isLoading = new BehaviorSubject(false); @Output() error = new EventEmitter(); + destroy$ = new Subject(); + basicChartOptions: ChartOptions = { // chart options not suitable for Chart.defaults.global scales: { xAxis: { @@ -146,7 +149,9 @@ export abstract class BarchartDirective ngOnChanges(changes: SimpleChanges) { if (changes.queryModel) { - this.queryModel.update.subscribe(this.refreshChart.bind(this)); + this.queryModel.update.pipe( + takeUntil(this.destroy$) + ).subscribe(this.refreshChart.bind(this)); } // new doc counts should be requested if query has changed if (this.changesRequireRefresh(changes)) { @@ -156,6 +161,10 @@ export abstract class BarchartDirective } } + ngOnDestroy(): void { + this.destroy$.next(); + } + /** check whether input changes should force reloading the data */ changesRequireRefresh(changes: SimpleChanges): boolean { const relevantChanges = [changes.corpus, changes.queryModel, changes.visualizedField, changes.frequencyMeasure] @@ -470,7 +479,7 @@ export abstract class BarchartDirective const datasets = this.getDatasets(); const options = this.chartOptions(datasets); - this.chart = new Chart('barchart', + this.chart = new Chart(barchartID, { type: this.chartType, data: { From 05c40d5c7043d206958e8d333844280f29a6304b Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 17:56:02 +0200 Subject: [PATCH 257/262] enable keyboard interaction on document dialogue --- .../src/app/search/search-results.component.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/search/search-results.component.html b/frontend/src/app/search/search-results.component.html index c61514805..2a42ad4c4 100644 --- a/frontend/src/app/search/search-results.component.html +++ b/frontend/src/app/search/search-results.component.html @@ -86,13 +86,15 @@

+ iaBalloon="view this document on its own page" autofocus + tabindex="0"> @@ -100,7 +102,8 @@

  + iaBalloon="view all documents from this {{contextDisplayName}}" + tabindex="0"> @@ -110,7 +113,8 @@

From d5b6fc91052d2c15c32ff38566fe427f516969a1 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 21 Jun 2023 18:05:27 +0200 Subject: [PATCH 258/262] accessibility in DialogComponent footer - enable keyboard interaction - show href for anchor element instead of using a callback function - display anchor with link layout to match its function --- frontend/src/app/dialog/dialog.component.html | 4 ++-- frontend/src/app/dialog/dialog.component.ts | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/dialog/dialog.component.html b/frontend/src/app/dialog/dialog.component.html index 8009e39e0..88c2ccdb4 100644 --- a/frontend/src/app/dialog/dialog.component.html +++ b/frontend/src/app/dialog/dialog.component.html @@ -1,8 +1,8 @@ {{title}} -
+
- + diff --git a/frontend/src/app/dialog/dialog.component.ts b/frontend/src/app/dialog/dialog.component.ts index 7730761ee..9ef1a9f0f 100644 --- a/frontend/src/app/dialog/dialog.component.ts +++ b/frontend/src/app/dialog/dialog.component.ts @@ -57,8 +57,4 @@ export class DialogComponent implements OnDestroy, OnInit { ngOnDestroy(): void { this.dialogEventSubscription.unsubscribe(); } - - navigate(): void { - this.router.navigate(this.footerRouterLink); - } } From 695d05f02e331d01f27ef0ebd15323fbcd111340 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 22 Jun 2023 13:17:40 +0200 Subject: [PATCH 259/262] Update rechtspraak.md --- backend/corpora/rechtspraak/description/rechtspraak.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/corpora/rechtspraak/description/rechtspraak.md b/backend/corpora/rechtspraak/description/rechtspraak.md index 3840b7ea2..f21011e4a 100644 --- a/backend/corpora/rechtspraak/description/rechtspraak.md +++ b/backend/corpora/rechtspraak/description/rechtspraak.md @@ -1,3 +1,3 @@ Court rulings published to [uitspraken.rechtspraak.nl](uitspraken.rechtspraak.nl). This corpus contains a portion of all court rulings in the Netherlands; rechtspraak.nl contains [detailed information about the selection criteria](https://www.rechtspraak.nl/Uitspraken/Paginas/Selectiecriteria.aspx). Rulings are anonimised. -Be aware that while the rulings on rechtspraak.nl are updated regularly, the rulings on I-analyzer are not, and may be out of date. (You can see the latest available publication date in the corpus.) We are working on this! +Be aware that while the rulings on rechtspraak.nl are updated regularly, the rulings on I-analyzer are not, and may be out of date. The latest version fo the corpus has been retrieved on 04-10-2022. We are working on this! From 059184adc0e571ab521966710c702f41d9e760b6 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 22 Jun 2023 13:18:02 +0200 Subject: [PATCH 260/262] Update rechtspraak.md --- backend/corpora/rechtspraak/description/rechtspraak.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/corpora/rechtspraak/description/rechtspraak.md b/backend/corpora/rechtspraak/description/rechtspraak.md index f21011e4a..2cd00a143 100644 --- a/backend/corpora/rechtspraak/description/rechtspraak.md +++ b/backend/corpora/rechtspraak/description/rechtspraak.md @@ -1,3 +1,3 @@ Court rulings published to [uitspraken.rechtspraak.nl](uitspraken.rechtspraak.nl). This corpus contains a portion of all court rulings in the Netherlands; rechtspraak.nl contains [detailed information about the selection criteria](https://www.rechtspraak.nl/Uitspraken/Paginas/Selectiecriteria.aspx). Rulings are anonimised. -Be aware that while the rulings on rechtspraak.nl are updated regularly, the rulings on I-analyzer are not, and may be out of date. The latest version fo the corpus has been retrieved on 04-10-2022. We are working on this! +Be aware that while the rulings on rechtspraak.nl are updated regularly, the rulings on I-analyzer are not, and may be out of date. The latest version of the corpus has been retrieved on 04-10-2022. We are working on this! From fc257d3f00a52383c96585eee2a3da6178ea6c67 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 22 Jun 2023 14:07:08 +0200 Subject: [PATCH 261/262] Fix test_field_stats --- .../visualization/tests/test_field_stats.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/visualization/tests/test_field_stats.py b/backend/visualization/tests/test_field_stats.py index 0390e8a33..46c4321f1 100644 --- a/backend/visualization/tests/test_field_stats.py +++ b/backend/visualization/tests/test_field_stats.py @@ -1,16 +1,18 @@ -from visualization.field_stats import * +from visualization.field_stats import count_field, count_total, report_coverage -def test_count(mock_corpus, test_es_client, select_small_mock_corpus, index_mock_corpus, mock_corpus_specs): - total_docs = mock_corpus_specs['total_docs'] - for field in mock_corpus_specs['fields']: - count = count_field(test_es_client, mock_corpus, field) +def test_count(small_mock_corpus, es_client, index_small_mock_corpus, small_mock_corpus_specs): + total_docs = small_mock_corpus_specs['total_docs'] + + for field in small_mock_corpus_specs['fields']: + count = count_field(es_client, small_mock_corpus, field) assert count == total_docs - assert count_total(test_es_client, mock_corpus) == total_docs + assert count_total(es_client, small_mock_corpus) == total_docs + -def test_report(mock_corpus, test_es_client, select_small_mock_corpus, index_mock_corpus, mock_corpus_specs): - report = report_coverage(mock_corpus) +def test_report(small_mock_corpus, es_client,index_small_mock_corpus, small_mock_corpus_specs): + report = report_coverage(small_mock_corpus) assert report == { 'date': 1.0, From 37aaf2c357556ff4618e130d549e449e2fa6f303 Mon Sep 17 00:00:00 2001 From: lukavdplas Date: Wed, 28 Jun 2023 15:28:57 +0200 Subject: [PATCH 262/262] v4.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index edc5eeb90..782a5ed47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "i-analyzer", - "version": "4.0.2", + "version": "4.1.0", "license": "MIT", "scripts": { "postinstall": "yarn install-back && yarn install-front",