• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

felleslosninger / einnsyn-backend / 22858128221

09 Mar 2026 02:24PM UTC coverage: 85.044% (-0.03%) from 85.069%
22858128221

push

github

web-flow
Merge pull request #619 from felleslosninger/EIN-4938-stotte-for-relativ-tid-i-sok

EIN-4938: Add support for relative time in SearchQueryService

2549 of 3425 branches covered (74.42%)

Branch coverage included in aggregate %.

7692 of 8617 relevant lines covered (89.27%)

3.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.94
src/main/java/no/einnsyn/backend/common/search/SearchQueryService.java
1
package no.einnsyn.backend.common.search;
2

3
import co.elastic.clients.elasticsearch._types.FieldValue;
4
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
5
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
6
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
7
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
8
import co.elastic.clients.elasticsearch._types.query_dsl.TermsQuery;
9
import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField;
10
import java.time.LocalDate;
11
import java.time.LocalDateTime;
12
import java.time.ZoneId;
13
import java.time.ZonedDateTime;
14
import java.time.format.DateTimeFormatter;
15
import java.time.format.DateTimeParseException;
16
import java.time.temporal.ChronoUnit;
17
import java.util.ArrayList;
18
import java.util.LinkedHashSet;
19
import java.util.List;
20
import no.einnsyn.backend.authentication.AuthenticationService;
21
import no.einnsyn.backend.common.exceptions.models.BadRequestException;
22
import no.einnsyn.backend.common.exceptions.models.EInnsynException;
23
import no.einnsyn.backend.common.queryparameters.models.FilterParameters;
24
import no.einnsyn.backend.entities.enhet.EnhetService;
25
import no.einnsyn.backend.validation.isodatetime.RelativeDateMath;
26
import org.springframework.stereotype.Service;
27
import org.springframework.util.StringUtils;
28

29
@Service
30
@SuppressWarnings("java:S1192") // Allow string literals
31
public class SearchQueryService {
32

33
  public enum DateBoundary {
3✔
34
    NONE,
6✔
35
    START_OF_DAY,
6✔
36
    END_OF_DAY
6✔
37
  }
38

39
  private static final List<String> allowedEntities =
4✔
40
      List.of("Journalpost", "Saksmappe", "Moetemappe", "Moetesak");
2✔
41
  public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
2✔
42
  private static final ZoneId NORWEGIAN_ZONE = ZoneId.of("Europe/Oslo");
4✔
43

44
  private final AuthenticationService authenticationService;
45
  private final EnhetService enhetService;
46

47
  public SearchQueryService(
48
      AuthenticationService authenticationService, EnhetService enhetService) {
2✔
49
    this.authenticationService = authenticationService;
3✔
50
    this.enhetService = enhetService;
3✔
51
  }
1✔
52

53
  private String toElasticsearchDateValue(String dateString, DateBoundary boundary)
54
      throws BadRequestException {
55
    if (dateString == null) {
2!
56
      return null;
×
57
    }
58

59
    // Relative date math is passed through as-is. Callers must use rounding explicitly when they
60
    // want day-style boundaries, e.g. "now/d" or "now-1d/d".
61
    if (RelativeDateMath.isValid(dateString)) {
3✔
62
      return dateString;
2✔
63
    }
64
    if (dateString.startsWith("now")) {
4!
65
      throw new BadRequestException("Invalid search query.");
×
66
    }
67

68
    ZonedDateTime zonedDateTime;
69

70
    // DateTime
71
    if (dateString.contains("T")) {
4✔
72
      // Try parsing zoned first; if no zone/offset is present, assume system default zone
73
      try {
74
        zonedDateTime = ZonedDateTime.parse(dateString);
3✔
75
      } catch (DateTimeParseException e) {
×
76
        var localDateTime = LocalDateTime.parse(dateString);
×
77
        zonedDateTime = localDateTime.atZone(NORWEGIAN_ZONE);
×
78
      }
1✔
79
    }
80

81
    // Date (no timestamp)
82
    else {
83
      zonedDateTime = LocalDate.parse(dateString).atStartOfDay(NORWEGIAN_ZONE);
5✔
84
      // Adjust to start or end of day if needed
85
      zonedDateTime =
1✔
86
          switch (boundary) {
3!
87
            case START_OF_DAY -> zonedDateTime.withHour(0).withMinute(0).withSecond(0).withNano(0);
×
88
            case END_OF_DAY ->
89
                zonedDateTime.withHour(23).withMinute(59).withSecond(59).withNano(999_999_999);
10✔
90
            case NONE -> zonedDateTime;
1✔
91
          };
92
    }
93

94
    return zonedDateTime.format(FORMATTER);
4✔
95
  }
96

97
  private Query getDateRangeQuery(
98
      String fieldName,
99
      String originalDateString,
100
      String elasticsearchDateValue,
101
      boolean inclusiveTo) {
102
    return RangeQuery.of(
7✔
103
            r ->
104
                r.date(
8✔
105
                    d -> {
106
                      d.field(fieldName);
4✔
107
                      if (inclusiveTo) {
2✔
108
                        d.lte(elasticsearchDateValue);
5✔
109
                      } else {
110
                        d.gte(elasticsearchDateValue);
4✔
111
                      }
112
                      if (RelativeDateMath.isValid(originalDateString)) {
3✔
113
                        d.timeZone(NORWEGIAN_ZONE.getId());
5✔
114
                      }
115
                      return d;
2✔
116
                    }))
117
        ._toQuery();
1✔
118
  }
119

120
  /**
121
   * Resolve IDs from identifiers like orgnummer, email, ...
122
   *
123
   * @param enhetIdentifiers the list of identifiers to resolve
124
   * @return the list of resolved Enhet IDs
125
   * @throws BadRequestException if an Enhet is not found
126
   */
127
  private List<String> resolveEnhetIds(List<String> enhetIdentifiers) throws BadRequestException {
128
    var enhetIds = new ArrayList<String>(enhetIdentifiers.size());
6✔
129
    for (var identifier : enhetIdentifiers) {
10✔
130
      var enhetId = enhetService.resolveId(identifier);
5✔
131
      if (enhetId == null) {
2!
132
        throw new BadRequestException("Enhet not found: " + identifier);
×
133
      }
134
      enhetIds.add(enhetId);
4✔
135
    }
1✔
136
    return enhetIds;
2✔
137
  }
138

139
  /**
140
   * Adds a filter to the bool query.
141
   *
142
   * @param bqb the bool query builder
143
   * @param propertyName the name of the property to filter on
144
   * @param list the list of values to filter by
145
   */
146
  private void addFilter(BoolQuery.Builder bqb, String propertyName, List<String> list) {
147
    if (list != null && !list.isEmpty()) {
5!
148
      var fieldValueList = list.stream().map(FieldValue::of).toList();
6✔
149
      bqb.filter(
6✔
150
          TermsQuery.of(tqb -> tqb.field(propertyName).terms(tqfb -> tqfb.value(fieldValueList)))
12✔
151
              ._toQuery());
3✔
152
    }
153
  }
1✔
154

155
  /**
156
   * Adds a must-not clause to the bool query.
157
   *
158
   * @param bqb the bool query builder
159
   * @param propertyName the name of the property to exclude
160
   * @param list the list of values to exclude
161
   */
162
  private void addMustNot(BoolQuery.Builder bqb, String propertyName, List<String> list) {
163
    if (list != null && !list.isEmpty()) {
5!
164
      var fieldValueList = list.stream().map(FieldValue::of).toList();
6✔
165
      bqb.mustNot(
6✔
166
          TermsQuery.of(tqb -> tqb.field(propertyName).terms(tqfb -> tqfb.value(fieldValueList)))
12✔
167
              ._toQuery());
3✔
168
    }
169
  }
1✔
170

171
  /**
172
   * Build a ES Query from the given search parameters.
173
   *
174
   * @param filterParameters the filter parameters
175
   * @return the bool query builder
176
   * @throws EInnsynException if an error occurs
177
   */
178
  public BoolQuery.Builder getQueryBuilder(FilterParameters filterParameters)
179
      throws EInnsynException {
180
    return getQueryBuilder(filterParameters, false);
5✔
181
  }
182

183
  /**
184
   * Build a ES Query from the given search parameters.
185
   *
186
   * @param filterParameters the filter parameters
187
   * @param uncensored whether to exclude sensitive fields or not
188
   * @return the bool query builder
189
   * @throws EInnsynException if an error occurs
190
   */
191
  public BoolQuery.Builder getQueryBuilder(FilterParameters filterParameters, boolean uncensored)
192
      throws EInnsynException {
193
    var rootBoolQueryBuilder = new BoolQuery.Builder();
4✔
194

195
    // Filter by entity. We don't want unexpected entities (Innsynskrav, Downloads, ...), so we'll
196
    // always filter by entities.
197
    if (filterParameters.getEntity() != null) {
3✔
198
      addFilter(rootBoolQueryBuilder, "type", filterParameters.getEntity());
7✔
199
    } else {
200
      addFilter(rootBoolQueryBuilder, "type", allowedEntities);
5✔
201
    }
202

203
    // Exclude hidden enhets and unaccessible documents
204
    if (!uncensored) {
2✔
205
      var authenticatedEnhetId = authenticationService.getEnhetId();
4✔
206
      var authenticatedSubtreeIdList = enhetService.getSubtreeIdList(authenticatedEnhetId);
5✔
207

208
      // Filter hidden enhets that the user is not authenticated for
209
      var hiddenEnhetList = enhetService.findHidden();
4✔
210
      var hiddenIdList =
1✔
211
          hiddenEnhetList.stream()
2✔
212
              .map(e -> e.getId())
3✔
213
              .filter(e -> !authenticatedSubtreeIdList.contains(e))
1!
214
              .toList();
2✔
215
      if (!hiddenIdList.isEmpty()) {
3!
216
        addMustNot(rootBoolQueryBuilder, "administrativEnhetTransitive", hiddenIdList);
×
217
      }
218
    }
219

220
    // Exclude unaccessible documents
221
    if (!uncensored) {
2✔
222
      var authenticatedEnhetId = authenticationService.getEnhetId();
4✔
223
      var accessibleAfterBoolQueryBuilder = new BoolQuery.Builder();
4✔
224
      accessibleAfterBoolQueryBuilder.minimumShouldMatch("1");
4✔
225

226
      // Allow documents with a valid accessibleAfter
227
      accessibleAfterBoolQueryBuilder.should(
4✔
228
          RangeQuery.of(r -> r.date(d -> d.field("accessibleAfter").lte("now")))._toQuery());
15✔
229

230
      // If logged in, allow documents with a valid administrativEnhet
231
      if (authenticatedEnhetId != null) {
2✔
232
        var authenticatedEnhetFieldValues = List.of(FieldValue.of(authenticatedEnhetId));
4✔
233
        accessibleAfterBoolQueryBuilder.should(
7✔
234
            new TermsQuery.Builder()
235
                .field("administrativEnhetTransitive")
5✔
236
                .terms(new TermsQueryField.Builder().value(authenticatedEnhetFieldValues).build())
4✔
237
                .build()
1✔
238
                ._toQuery());
3✔
239
      }
240

241
      rootBoolQueryBuilder.filter(accessibleAfterBoolQueryBuilder.build()._toQuery());
8✔
242
    }
243

244
    // Filter by search query
245
    var queryString = filterParameters.getQuery();
3✔
246
    if (StringUtils.hasText(queryString)) {
3✔
247
      rootBoolQueryBuilder.must(
3✔
248
          uncensored
2✔
249
              ? getSearchStringQuery(
8✔
250
                  queryString,
251
                  List.of(
3✔
252
                      "search_id",
253
                      "search_innhold",
254
                      "search_innhold_SENSITIV",
255
                      "search_tittel^3",
256
                      "search_tittel_SENSITIV^3"),
257
                  3.0f,
258
                  1.0f)
259
              : getSearchStringQuery(
7✔
260
                  queryString,
261
                  List.of("search_id", "search_innhold_SENSITIV", "search_tittel_SENSITIV^3"),
4✔
262
                  List.of("search_id", "search_innhold", "search_tittel^3"),
3✔
263
                  3.0f,
264
                  2.0f));
265
    }
266

267
    // Filter by tittel
268
    if (filterParameters.getTittel() != null) {
3✔
269
      for (var tittel : filterParameters.getTittel()) {
11✔
270
        if (StringUtils.hasText(tittel)) {
3!
271
          rootBoolQueryBuilder.filter(
3✔
272
              uncensored
2!
273
                  ? getSearchStringQuery(tittel, List.of("search_tittel", "search_tittel_SENSITIV"))
×
274
                  : getSearchStringQuery(
5✔
275
                      tittel, List.of("search_tittel_SENSITIV"), List.of("search_tittel")));
3✔
276
        }
277
      }
1✔
278
    }
279

280
    // Filter by skjermingshjemmel
281
    if (filterParameters.getSkjermingshjemmel() != null) {
3✔
282
      for (var skjermingshjemmel : filterParameters.getSkjermingshjemmel()) {
11✔
283
        if (StringUtils.hasText(skjermingshjemmel)) {
3!
284
          rootBoolQueryBuilder.filter(
5✔
285
              getSearchStringQuery(skjermingshjemmel, List.of("skjerming.skjermingshjemmel")));
4✔
286
        }
287
      }
1✔
288
    }
289

290
    // Filter by korrespondansepartNavn
291
    if (filterParameters.getKorrespondansepartNavn() != null) {
3✔
292
      for (var korrespondansepartNavn : filterParameters.getKorrespondansepartNavn()) {
11✔
293
        if (StringUtils.hasText(korrespondansepartNavn)) {
3!
294
          rootBoolQueryBuilder.filter(
5✔
295
              getSearchStringQuery(
3✔
296
                  korrespondansepartNavn,
297
                  List.of("korrespondansepart.korrespondansepartNavn_SENSITIV"),
2✔
298
                  List.of("korrespondansepart.korrespondansepartNavn")));
1✔
299
        }
300
      }
1✔
301
    }
302

303
    // Filter by saksaar
304
    addFilter(rootBoolQueryBuilder, "saksaar", filterParameters.getSaksaar());
6✔
305

306
    // Filter by sakssekvensnummer
307
    addFilter(rootBoolQueryBuilder, "sakssekvensnummer", filterParameters.getSakssekvensnummer());
6✔
308

309
    // Filter by saksnummer
310
    addFilter(rootBoolQueryBuilder, "saksnummer", filterParameters.getSaksnummer());
6✔
311

312
    // Filter by journalpostnummer
313
    addFilter(rootBoolQueryBuilder, "journalpostnummer", filterParameters.getJournalpostnummer());
6✔
314

315
    // Filter by journalsekvensnummer
316
    addFilter(
5✔
317
        rootBoolQueryBuilder, "journalsekvensnummer", filterParameters.getJournalsekvensnummer());
1✔
318

319
    // Filter by moetesaksaar
320
    addFilter(rootBoolQueryBuilder, "møtesaksår", filterParameters.getMoetesaksaar());
6✔
321

322
    // Filter by moetesakssekvensnummer
323
    addFilter(
5✔
324
        rootBoolQueryBuilder,
325
        "møtesakssekvensnummer",
326
        filterParameters.getMoetesakssekvensnummer());
1✔
327

328
    // Matches against administrativEnhet or children
329
    if (filterParameters.getAdministrativEnhet() != null) {
3✔
330
      var enhetList = resolveEnhetIds(filterParameters.getAdministrativEnhet());
5✔
331
      addFilter(rootBoolQueryBuilder, "administrativEnhetTransitive", enhetList);
5✔
332
    }
333

334
    // Exact matches against administrativEnhet
335
    if (filterParameters.getAdministrativEnhetExact() != null) {
3✔
336
      var enhetList = resolveEnhetIds(filterParameters.getAdministrativEnhetExact());
5✔
337
      addFilter(rootBoolQueryBuilder, "administrativEnhet", enhetList);
5✔
338
    }
339

340
    // Exclude documents from given administrativEnhet or children
341
    if (filterParameters.getExcludeAdministrativEnhet() != null) {
3✔
342
      var enhetList = resolveEnhetIds(filterParameters.getExcludeAdministrativEnhet());
5✔
343
      addMustNot(rootBoolQueryBuilder, "administrativEnhetTransitive", enhetList);
5✔
344
    }
345

346
    // Exclude documents from given administrativEnhet
347
    if (filterParameters.getExcludeAdministrativEnhetExact() != null) {
3✔
348
      var enhetList = resolveEnhetIds(filterParameters.getExcludeAdministrativEnhetExact());
5✔
349
      addMustNot(rootBoolQueryBuilder, "administrativEnhet", enhetList);
5✔
350
    }
351

352
    // Filter by publisertDatoTo
353
    if (filterParameters.getPublisertDatoTo() != null) {
3✔
354
      var originalDate = filterParameters.getPublisertDatoTo();
3✔
355
      var date = toElasticsearchDateValue(originalDate, DateBoundary.END_OF_DAY);
5✔
356
      rootBoolQueryBuilder.filter(getDateRangeQuery("publisertDato", originalDate, date, true));
11✔
357
    }
358

359
    // Filter by publisertDatoFrom
360
    if (filterParameters.getPublisertDatoFrom() != null) {
3✔
361
      var originalDate = filterParameters.getPublisertDatoFrom();
3✔
362
      var date = toElasticsearchDateValue(originalDate, DateBoundary.NONE);
5✔
363
      rootBoolQueryBuilder.filter(getDateRangeQuery("publisertDato", originalDate, date, false));
11✔
364
    }
365

366
    // Filter by oppdatertDatoTo
367
    if (filterParameters.getOppdatertDatoTo() != null) {
3✔
368
      var originalDate = filterParameters.getOppdatertDatoTo();
3✔
369
      var date = toElasticsearchDateValue(originalDate, DateBoundary.END_OF_DAY);
5✔
370
      rootBoolQueryBuilder.filter(getDateRangeQuery("oppdatertDato", originalDate, date, true));
11✔
371
    }
372

373
    // Filter by oppdatertDatoFrom
374
    if (filterParameters.getOppdatertDatoFrom() != null) {
3✔
375
      var originalDate = filterParameters.getOppdatertDatoFrom();
3✔
376
      var date = toElasticsearchDateValue(originalDate, DateBoundary.NONE);
5✔
377
      rootBoolQueryBuilder.filter(getDateRangeQuery("oppdatertDato", originalDate, date, false));
11✔
378
    }
379

380
    // Filter by dokumentetsDatoTo
381
    if (filterParameters.getDokumentetsDatoTo() != null) {
3✔
382
      var originalDate = filterParameters.getDokumentetsDatoTo();
3✔
383
      var date = toElasticsearchDateValue(originalDate, DateBoundary.END_OF_DAY);
5✔
384
      rootBoolQueryBuilder.filter(getDateRangeQuery("dokumentetsDato", originalDate, date, true));
11✔
385
    }
386

387
    // Filter by dokumentetsDatoFrom
388
    if (filterParameters.getDokumentetsDatoFrom() != null) {
3✔
389
      var originalDate = filterParameters.getDokumentetsDatoFrom();
3✔
390
      var date = toElasticsearchDateValue(originalDate, DateBoundary.NONE);
5✔
391
      rootBoolQueryBuilder.filter(getDateRangeQuery("dokumentetsDato", originalDate, date, false));
11✔
392
    }
393

394
    // Filter by journaldatoTo
395
    if (filterParameters.getJournaldatoTo() != null) {
3✔
396
      var originalDate = filterParameters.getJournaldatoTo();
3✔
397
      var date = toElasticsearchDateValue(originalDate, DateBoundary.END_OF_DAY);
5✔
398
      rootBoolQueryBuilder.filter(getDateRangeQuery("journaldato", originalDate, date, true));
11✔
399
    }
400

401
    // Filter by journaldatoFrom
402
    if (filterParameters.getJournaldatoFrom() != null) {
3✔
403
      var originalDate = filterParameters.getJournaldatoFrom();
3✔
404
      var date = toElasticsearchDateValue(originalDate, DateBoundary.NONE);
5✔
405
      rootBoolQueryBuilder.filter(getDateRangeQuery("journaldato", originalDate, date, false));
11✔
406
    }
407

408
    // Filter by moetedatoTo
409
    if (filterParameters.getMoetedatoTo() != null) {
3✔
410
      var originalDate = filterParameters.getMoetedatoTo();
3✔
411
      var date = toElasticsearchDateValue(originalDate, DateBoundary.END_OF_DAY);
5✔
412
      rootBoolQueryBuilder.filter(getDateRangeQuery("moetedato", originalDate, date, true));
11✔
413
    }
414

415
    // Filter by moetedatoFrom
416
    if (filterParameters.getMoetedatoFrom() != null) {
3✔
417
      var originalDate = filterParameters.getMoetedatoFrom();
3✔
418
      var date = toElasticsearchDateValue(originalDate, DateBoundary.NONE);
5✔
419
      rootBoolQueryBuilder.filter(getDateRangeQuery("moetedato", originalDate, date, false));
11✔
420
    }
421

422
    // Filter by standardDatoTo
423
    if (filterParameters.getStandardDatoTo() != null) {
3✔
424
      var originalDate = filterParameters.getStandardDatoTo();
3✔
425
      var date = toElasticsearchDateValue(originalDate, DateBoundary.END_OF_DAY);
5✔
426
      rootBoolQueryBuilder.filter(getDateRangeQuery("standardDato", originalDate, date, true));
11✔
427
    }
428

429
    // Filter by standardDatoFrom
430
    if (filterParameters.getStandardDatoFrom() != null) {
3✔
431
      var originalDate = filterParameters.getStandardDatoFrom();
3✔
432
      var date = toElasticsearchDateValue(originalDate, DateBoundary.NONE);
5✔
433
      rootBoolQueryBuilder.filter(getDateRangeQuery("standardDato", originalDate, date, false));
11✔
434
    }
435

436
    // Filter by fulltext
437
    if (filterParameters.getFulltext() != null) {
3✔
438
      rootBoolQueryBuilder.filter(
5✔
439
          TermQuery.of(tqb -> tqb.field("fulltext").value(filterParameters.getFulltext()))
9✔
440
              ._toQuery());
3✔
441
    }
442

443
    // Filter by journalposttype
444
    if (filterParameters.getJournalposttype() != null) {
3✔
445
      addFilter(rootBoolQueryBuilder, "journalposttype", filterParameters.getJournalposttype());
6✔
446
    }
447

448
    // Get specific IDs
449
    addFilter(rootBoolQueryBuilder, "id", filterParameters.getIds());
6✔
450

451
    return rootBoolQueryBuilder;
2✔
452
  }
453

454
  /**
455
   * /** Get a sensitive query that handles uncensored/censored searches.
456
   *
457
   * @param queryString the query string to search for
458
   * @param sensitiveFields the list of sensitive fields
459
   * @param nonSensitiveFields the list of non-sensitive fields
460
   * @return the constructed query
461
   */
462
  private static Query getSearchStringQuery(
463
      String queryString, List<String> sensitiveFields, List<String> nonSensitiveFields) {
464
    return getSearchStringQuery(queryString, sensitiveFields, nonSensitiveFields, 1.0f, 1.0f);
7✔
465
  }
466

467
  /**
468
   * Get a sensitive query that handles uncensored/censored searches.
469
   *
470
   * @param queryString the search query string
471
   * @param sensitiveFields the list of sensitive field names to search in
472
   * @param nonSensitiveFields the list of non-sensitive field names to search in
473
   * @param exactBoost the boost factor for exact matches
474
   * @param looseBoost the boost factor for loose matches
475
   * @return the constructed query
476
   */
477
  private static Query getSearchStringQuery(
478
      String queryString,
479
      List<String> sensitiveFields,
480
      List<String> nonSensitiveFields,
481
      float exactBoost,
482
      float looseBoost) {
483
    var boolQueryBuilder = new BoolQuery.Builder();
4✔
484
    boolQueryBuilder.minimumShouldMatch("1");
4✔
485

486
    // Match sensitive fields for documents from the past year only
487
    // Round to start of day to ensure consistent query hashing for preference-based shard routing
488
    var lastYear =
1✔
489
        ZonedDateTime.now(NORWEGIAN_ZONE)
2✔
490
            .truncatedTo(ChronoUnit.DAYS)
2✔
491
            .minusYears(1)
2✔
492
            .format(FORMATTER);
2✔
493
    var gteLastYear =
2✔
494
        RangeQuery.of(r -> r.date(d -> d.field("publisertDato").gte(lastYear)))._toQuery();
15✔
495

496
    // For recent documents, evaluate the query against the union of sensitive and non-sensitive
497
    // fields so negation semantics (e.g. -word) are applied across both field groups.
498
    var recentFields = new LinkedHashSet<String>(nonSensitiveFields);
5✔
499
    recentFields.addAll(sensitiveFields);
4✔
500
    var recentDocumentsQuery =
6✔
501
        new BoolQuery.Builder()
502
            .filter(gteLastYear)
3✔
503
            .must(
1✔
504
                getSearchStringQuery(
3✔
505
                    queryString, List.copyOf(recentFields), exactBoost, looseBoost))
3✔
506
            .build();
2✔
507

508
    // For older (or missing publisertDato) documents, only evaluate non-sensitive fields.
509
    var olderDocumentsQuery =
6✔
510
        new BoolQuery.Builder()
511
            .mustNot(gteLastYear)
5✔
512
            .must(getSearchStringQuery(queryString, nonSensitiveFields, exactBoost, looseBoost))
4✔
513
            .build();
2✔
514
    boolQueryBuilder.should(b -> b.bool(recentDocumentsQuery));
9✔
515
    boolQueryBuilder.should(b -> b.bool(olderDocumentsQuery));
9✔
516

517
    return boolQueryBuilder.build()._toQuery();
4✔
518
  }
519

520
  /**
521
   * A direct wrapper around SearchQueryParser that doesn't consider sensitive fields.
522
   *
523
   * @param searchString the search string
524
   * @param fields the fields to search in
525
   * @return the constructed query
526
   */
527
  private static Query getSearchStringQuery(String searchString, List<String> fields) {
528
    return SearchQueryParser.parse(searchString, fields);
4✔
529
  }
530

531
  /**
532
   * A direct wrapper around SearchQueryParser that doesn't consider sensitive fields.
533
   *
534
   * @param searchString the search query string
535
   * @param fields the list of field names to search in
536
   * @param exactBoost the boost factor for exact matches
537
   * @param looseBoost the boost factor for loose matches
538
   * @return the constructed query
539
   */
540
  private static Query getSearchStringQuery(
541
      String searchString, List<String> fields, float exactBoost, float looseBoost) {
542
    return SearchQueryParser.parse(searchString, fields, exactBoost, looseBoost);
6✔
543
  }
544
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc