RED-10290: Improve SearchImplementation logic for dictionaries

* use a different implementation as the DoubleArrayTrieDictionarySearch has a massive performance drop as soon as a certain threshold in terms of dictionary size is reached
This commit is contained in:
maverickstuder 2024-10-30 17:09:39 +01:00
parent a9e4fa31bf
commit d72e35723c
12 changed files with 122190 additions and 302 deletions

View File

@ -63,6 +63,7 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}")
implementation("org.ahocorasick:ahocorasick:0.9.0") implementation("org.ahocorasick:ahocorasick:0.9.0")
implementation("com.hankcs:aho-corasick-double-array-trie:1.2.2") implementation("com.hankcs:aho-corasick-double-array-trie:1.2.2")
implementation("com.github.roklenarcic:aho-corasick:1.2")
implementation("org.javassist:javassist:3.29.2-GA") implementation("org.javassist:javassist:3.29.2-GA")
implementation("org.drools:drools-engine:${droolsVersion}") implementation("org.drools:drools-engine:${droolsVersion}")

View File

@ -0,0 +1,130 @@
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.*;
import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
public abstract class AbstractDictionarySearch implements DictionarySearch {
protected final Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap;
public AbstractDictionarySearch(Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap) {
this.keyWordToIdentifiersMap = keyWordToIdentifiersMap;
}
@Override
public Stream<MatchTextRange> getBoundaries(CharSequence text) {
TextContext textContext = new TextContext(text);
return getMatchTextRangeStream(textContext);
}
@Override
public Stream<MatchTextRange> getBoundaries(CharSequence text, TextRange region) {
CharSequence subText = text.subSequence(region.start(), region.end());
TextContext textContext = new TextContext(subText, region.start());
return getMatchTextRangeStream(textContext);
}
@Override
public Stream<MatchTextRange> getBoundaries(TextBlock textBlock) {
return getBoundaries(textBlock, textBlock.getTextRange());
}
@Override
public Stream<MatchPosition> getMatches(String text) {
TextContext textContext = new TextContext(text);
List<MatchPosition> matches = new ArrayList<>();
parseText(textContext.getLowerText(), (begin, end, value) -> addMatchPositionsForHit(textContext, matches, new Hit(begin, end, value)));
return matches.stream();
}
private Stream<MatchTextRange> getMatchTextRangeStream(TextContext textContext) {
List<MatchTextRange> matches = new ArrayList<>();
parseText(textContext.getLowerText(), (begin, end, value) -> addMatchesForHit(textContext, matches, new Hit(begin, end, value)));
return matches.stream();
}
protected abstract void parseText(CharSequence text, HitHandler handler);
protected void addMatchesForHit(TextContext textContext, List<MatchTextRange> matches, Hit hit) {
int start = textContext.getStart(hit.begin);
int end = textContext.getEnd(hit.end);
String matchedText = textContext.getMatchedText(hit.begin, hit.end);
List<DictionaryIdentifierWithKeyword> idWithKeywords = hit.value;
for (DictionaryIdentifierWithKeyword idkw : idWithKeywords) {
if (idkw.identifier().caseSensitive()) {
if (matchedText.equals(idkw.keyword())) {
matches.add(new MatchTextRange(idkw.identifier(), new TextRange(start, end)));
}
} else {
matches.add(new MatchTextRange(idkw.identifier(), new TextRange(start, end)));
}
}
}
protected void addMatchPositionsForHit(TextContext textContext, List<MatchPosition> matches, Hit hit) {
int start = textContext.getStart(hit.begin);
int end = textContext.getEnd(hit.end);
String matchedText = textContext.getMatchedText(hit.begin, hit.end);
List<DictionaryIdentifierWithKeyword> idWithKeywords = hit.value;
for (DictionaryIdentifierWithKeyword idkw : idWithKeywords) {
MatchPosition matchPosition = new MatchPosition(idkw.identifier(), start, end);
if (idkw.identifier().caseSensitive()) {
if (matchedText.equals(idkw.keyword())) {
matches.add(matchPosition);
}
} else {
matches.add(matchPosition);
}
}
}
protected interface HitHandler {
void handle(int begin, int end, List<DictionaryIdentifierWithKeyword> value);
}
protected static class Hit {
final int begin;
final int end;
final List<DictionaryIdentifierWithKeyword> value;
Hit(int begin, int end, List<DictionaryIdentifierWithKeyword> value) {
this.begin = begin;
this.end = end;
this.value = value;
}
}
}

View File

@ -0,0 +1,32 @@
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.List;
import java.util.Map;
import com.roklenarcic.util.strings.AhoCorasickMap;
import com.roklenarcic.util.strings.MapMatchListener;
import com.roklenarcic.util.strings.StringMap;
public class AhoCorasickMapDictionarySearch extends AbstractDictionarySearch {
private final StringMap<List<DictionaryIdentifierWithKeyword>> map;
public AhoCorasickMapDictionarySearch(Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap) {
super(keyWordToIdentifiersMap);
map = new AhoCorasickMap<>(keyWordToIdentifiersMap.keySet(), keyWordToIdentifiersMap.values(), false);
}
@Override
protected void parseText(CharSequence text, HitHandler handler) {
MapMatchListener<List<DictionaryIdentifierWithKeyword>> listener = (haystack, startPosition, endPosition, value) -> {
handler.handle(startPosition, endPosition, value);
return true;
};
map.match(text.toString(), listener);
}
}

View File

@ -3,6 +3,7 @@ package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -19,57 +20,70 @@ import lombok.SneakyThrows;
@RequiredArgsConstructor @RequiredArgsConstructor
public class DictionaryFactory { public class DictionaryFactory {
@SneakyThrows @SneakyThrows
public Dictionary create(List<DictionaryModel> dictionaryModels, DictionaryVersion dictionaryVersion) { public Dictionary create(List<DictionaryModel> dictionaryModels, DictionaryVersion dictionaryVersion) {
Map<DictionaryIdentifier, List<String>> combinedMap = generateCombinedDictionaryValuesMap(dictionaryModels); Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap = computeStringIdentifiersMap(dictionaryModels);
DictionarySearch dictionarySearch = new DoubleArrayTrieDictionarySearch(combinedMap); DictionarySearch dictionarySearch = getDictionarySearch(keyWordToIdentifiersMap);
return new Dictionary(dictionaryModels, dictionaryVersion, dictionarySearch); return new Dictionary(dictionaryModels, dictionaryVersion, dictionarySearch);
} }
private Map<DictionaryIdentifier, List<String>> generateCombinedDictionaryValuesMap(List<DictionaryModel> dictionaryModels) { private static DictionarySearch getDictionarySearch(Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap) {
// a more sophisticated selection of the dictionarySearch could be done here
// but as we do not have the need to fine-tune at the moment we use the all-rounder solution, which is the AhoCoraSickMapDictionarySearch
// based on this repository https://github.com/RokLenarcic/AhoCorasick
// This is an outline how a more complex dictionarySearch decision could be made:
// if (!redactionServiceSettings.isPriorityMode() && keyWordToIdentifiersMap.keySet().size() < 50_000) {
// dictionarySearch = new DoubleArrayTrieDictionarySearch(keyWordToIdentifiersMap);
// } else {
// dictionarySearch = new AhoCorasickMapDictionarySearch(keyWordToIdentifiersMap);
// }
return new AhoCorasickMapDictionarySearch(keyWordToIdentifiersMap);
}
protected static Map<String, List<DictionaryIdentifierWithKeyword>> computeStringIdentifiersMap(List<DictionaryModel> dictionaryModels) {
Map<String, List<DictionaryIdentifierWithKeyword>> stringToIdentifiersMap = new HashMap<>();
Map<DictionaryIdentifier, List<String>> combinedValuesMap = new HashMap<>();
for (DictionaryModel model : dictionaryModels) { for (DictionaryModel model : dictionaryModels) {
addDictionaryEntries(combinedValuesMap, model);
// Add entries for different entity types
addEntriesToMap(stringToIdentifiersMap, model, EntityType.ENTITY, model.getEntries(), false);
addEntriesToMap(stringToIdentifiersMap, model, EntityType.FALSE_POSITIVE, model.getFalsePositives(), false);
addEntriesToMap(stringToIdentifiersMap, model, EntityType.FALSE_RECOMMENDATION, model.getFalseRecommendations(), false);
if (model.isDossierDictionary()) {
addEntriesToMap(stringToIdentifiersMap, model, EntityType.DICTIONARY_REMOVAL, model.getEntries(), true);
}
} }
return combinedValuesMap;
return stringToIdentifiersMap;
} }
private void addDictionaryEntries(Map<DictionaryIdentifier, List<String>> combinedValuesMap, DictionaryModel model) { private static void addEntriesToMap(Map<String, List<DictionaryIdentifierWithKeyword>> stringToIdentifiersMap,
DictionaryModel model,
EntityType entityType,
Set<DictionaryEntryModel> entries,
boolean isDeleted) {
addValuesToMap(combinedValuesMap, createIdentifier(model, EntityType.ENTITY), filterValues(model.getEntries(), false)); DictionaryIdentifier identifier = new DictionaryIdentifier(model.getType(), entityType, model.isDossierDictionary(), !model.isCaseInsensitive());
addValuesToMap(combinedValuesMap, createIdentifier(model, EntityType.FALSE_POSITIVE), filterValues(model.getFalsePositives(), false));
addValuesToMap(combinedValuesMap, createIdentifier(model, EntityType.FALSE_RECOMMENDATION), filterValues(model.getFalseRecommendations(), false));
if (model.isDossierDictionary()) { List<String> values = entries.stream()
addValuesToMap(combinedValuesMap, createIdentifier(model, EntityType.DICTIONARY_REMOVAL), filterValues(model.getEntries(), true));
}
}
private DictionaryIdentifier createIdentifier(DictionaryModel model, EntityType entityType) {
return new DictionaryIdentifier(model.getType(), entityType, model.isDossierDictionary(), !model.isCaseInsensitive());
}
private List<String> filterValues(Set<DictionaryEntryModel> entries, boolean isDeleted) {
return entries.stream()
.filter(entry -> entry.isDeleted() == isDeleted) .filter(entry -> entry.isDeleted() == isDeleted)
.map(DictionaryEntry::getValue) .map(DictionaryEntry::getValue)
.toList(); .toList();
}
for (String value : values) {
private void addValuesToMap(Map<DictionaryIdentifier, List<String>> map, DictionaryIdentifier key, List<String> values) { DictionaryIdentifierWithKeyword idWithKeyword = new DictionaryIdentifierWithKeyword(identifier, value);
String key = value.toLowerCase(Locale.ROOT);
if (!values.isEmpty()) { stringToIdentifiersMap.computeIfAbsent(key, k -> new ArrayList<>()).add(idWithKeyword);
map.computeIfAbsent(key, k -> new ArrayList<>()).addAll(values);
} }
} }

View File

@ -0,0 +1,5 @@
package com.iqser.red.service.redaction.v1.server.model.dictionary;
public record DictionaryIdentifierWithKeyword(DictionaryIdentifier identifier, String keyword) {
}

View File

@ -1,177 +1,31 @@
package com.iqser.red.service.redaction.v1.server.model.dictionary; package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream;
import com.hankcs.algorithm.AhoCorasickDoubleArrayTrie; import com.hankcs.algorithm.AhoCorasickDoubleArrayTrie;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import lombok.Getter; public class DoubleArrayTrieDictionarySearch extends AbstractDictionarySearch {
public class DoubleArrayTrieDictionarySearch implements DictionarySearch {
private final AhoCorasickDoubleArrayTrie<List<DictionaryIdentifierWithKeyword>> trie; private final AhoCorasickDoubleArrayTrie<List<DictionaryIdentifierWithKeyword>> trie;
public DoubleArrayTrieDictionarySearch(Map<DictionaryIdentifier, List<String>> dictionaryValues) { public DoubleArrayTrieDictionarySearch(Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap) {
super(keyWordToIdentifiersMap);
trie = new AhoCorasickDoubleArrayTrie<>(); trie = new AhoCorasickDoubleArrayTrie<>();
trie.build(computeStringIdentifiersMap(dictionaryValues)); trie.build(keyWordToIdentifiersMap);
} }
private static Map<String, List<DictionaryIdentifierWithKeyword>> computeStringIdentifiersMap(Map<DictionaryIdentifier, List<String>> dictionaryValues) {
Map<String, List<DictionaryIdentifierWithKeyword>> stringToIdentifiersMap = new HashMap<>();
for (Map.Entry<DictionaryIdentifier, List<String>> entry : dictionaryValues.entrySet()) {
DictionaryIdentifier identifier = entry.getKey();
List<String> values = entry.getValue();
for (String value : values) {
DictionaryIdentifierWithKeyword idWithKeyword = new DictionaryIdentifierWithKeyword(identifier, value);
stringToIdentifiersMap.computeIfAbsent(value.toLowerCase(Locale.ROOT), k -> new ArrayList<>()).add(idWithKeyword);
}
}
return stringToIdentifiersMap;
}
@Override @Override
public Stream<MatchTextRange> getBoundaries(CharSequence text) { protected void parseText(CharSequence text, HitHandler handler) {
TextContext textContext = new TextContext(text);
return getMatchTextRangeStream(textContext);
}
@Override
public Stream<MatchTextRange> getBoundaries(CharSequence text, TextRange region) {
CharSequence subText = text.subSequence(region.start(), region.end());
TextContext textContext = new TextContext(subText, region.start());
return getMatchTextRangeStream(textContext);
}
@Override
public Stream<MatchTextRange> getBoundaries(TextBlock textBlock) {
return getBoundaries(textBlock, textBlock.getTextRange());
}
@Override
public Stream<MatchPosition> getMatches(String text) {
TextContext textContext = new TextContext(text);
List<MatchPosition> matches = new ArrayList<>();
List<AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>>> hits = trie.parseText(textContext.getLowerText());
List<AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>>> hits = trie.parseText(text);
for (AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>> hit : hits) { for (AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>> hit : hits) {
addMatchPositionsForHit(textContext, matches, hit); handler.handle(hit.begin, hit.end, hit.value);
} }
return matches.stream();
}
private Stream<MatchTextRange> getMatchTextRangeStream(TextContext textContext) {
List<MatchTextRange> matches = new ArrayList<>();
List<AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>>> hits = trie.parseText(textContext.getLowerText());
for (AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>> hit : hits) {
addMatchesForHit(textContext, matches, hit);
}
return matches.stream();
}
private void addMatchesForHit(TextContext textContext, List<MatchTextRange> matches, AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>> hit) {
int start = textContext.getStart(hit.begin);
int end = textContext.getEnd(hit.end);
String matchedText = textContext.getMatchedText(hit.begin, hit.end);
List<DictionaryIdentifierWithKeyword> idWithKeywords = hit.value;
for (DictionaryIdentifierWithKeyword idkw : idWithKeywords) {
if (idkw.identifier.caseSensitive()) {
if (matchedText.equals(idkw.keyword)) {
matches.add(new MatchTextRange(idkw.identifier, new TextRange(start, end)));
}
} else {
matches.add(new MatchTextRange(idkw.identifier, new TextRange(start, end)));
}
}
}
private void addMatchPositionsForHit(TextContext textContext, List<MatchPosition> matches, AhoCorasickDoubleArrayTrie.Hit<List<DictionaryIdentifierWithKeyword>> hit) {
int start = textContext.getStart(hit.begin);
int end = textContext.getEnd(hit.end);
String matchedText = textContext.getMatchedText(hit.begin, hit.end);
List<DictionaryIdentifierWithKeyword> idWithKeywords = hit.value;
for (DictionaryIdentifierWithKeyword idkw : idWithKeywords) {
MatchPosition matchPosition = new MatchPosition(idkw.identifier, start, end);
if (idkw.identifier.caseSensitive()) {
if (matchedText.equals(idkw.keyword)) {
matches.add(matchPosition);
}
} else {
matches.add(matchPosition);
}
}
}
private static class TextContext {
private final CharSequence text;
@Getter
private final String lowerText;
private final int offset;
TextContext(CharSequence text, int offset) {
this.text = text;
this.lowerText = text.toString().toLowerCase(Locale.ROOT);
this.offset = offset;
}
TextContext(CharSequence text) {
this(text, 0);
}
public int getStart(int hitBegin) {
return hitBegin + offset;
}
public int getEnd(int hitEnd) {
return hitEnd + offset;
}
public String getMatchedText(int hitBegin, int hitEnd) {
return text.subSequence(hitBegin, hitEnd).toString();
}
}
private record DictionaryIdentifierWithKeyword(DictionaryIdentifier identifier, String keyword) {
} }
} }

View File

@ -9,7 +9,7 @@ import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange; import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
public class TrieDictionarySearch implements DictionarySearch { public class DoubleTrieDictionarySearch implements DictionarySearch {
private final Map<DictionaryIdentifier, List<String>> caseSensitiveEntries = new HashMap<>(); private final Map<DictionaryIdentifier, List<String>> caseSensitiveEntries = new HashMap<>();
private final Map<DictionaryIdentifier, List<String>> caseInsensitiveEntries = new HashMap<>(); private final Map<DictionaryIdentifier, List<String>> caseInsensitiveEntries = new HashMap<>();
@ -17,7 +17,7 @@ public class TrieDictionarySearch implements DictionarySearch {
private final DictionaryIdentifierTrie caseInsensitiveTrie; private final DictionaryIdentifierTrie caseInsensitiveTrie;
public TrieDictionarySearch(Map<DictionaryIdentifier, List<String>> dictionaryValues) { public DoubleTrieDictionarySearch(Map<DictionaryIdentifier, List<String>> dictionaryValues) {
for (Map.Entry<DictionaryIdentifier, List<String>> entry : dictionaryValues.entrySet()) { for (Map.Entry<DictionaryIdentifier, List<String>> entry : dictionaryValues.entrySet()) {
DictionaryIdentifier identifier = entry.getKey(); DictionaryIdentifier identifier = entry.getKey();

View File

@ -0,0 +1,46 @@
package com.iqser.red.service.redaction.v1.server.model.dictionary;
import java.util.Locale;
import lombok.Getter;
public class TextContext {
private final CharSequence text;
@Getter
private final String lowerText;
private final int offset;
TextContext(CharSequence text, int offset) {
this.text = text;
this.lowerText = text.toString().toLowerCase(Locale.ROOT);
this.offset = offset;
}
TextContext(CharSequence text) {
this(text, 0);
}
public int getStart(int hitBegin) {
return hitBegin + offset;
}
public int getEnd(int hitEnd) {
return hitEnd + offset;
}
public String getMatchedText(int hitBegin, int hitEnd) {
return text.subSequence(hitBegin, hitEnd).toString();
}
}

View File

@ -1,6 +1,7 @@
package com.iqser.red.service.redaction.v1.server.service; package com.iqser.red.service.redaction.v1.server.service;
import java.awt.Color; import java.awt.Color;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -111,18 +112,16 @@ public class DictionaryService {
DictionaryVersion latestVersion = updateDictionary(dossierTemplateId, dossierId); DictionaryVersion latestVersion = updateDictionary(dossierTemplateId, dossierId);
Set<DictionaryIncrementValue> newValues = new HashSet<>(); Set<DictionaryIncrementValue> newValues = Collections.synchronizedSet(new HashSet<>());
List<DictionaryModel> templateDictionaries = dictionaryCacheService.getDossierTemplateDictionary(dossierTemplateId).getDictionary(); List<DictionaryModel> templateDictionaries = dictionaryCacheService.getDossierTemplateDictionary(dossierTemplateId).getDictionary();
templateDictionaries.forEach(dictionaryModel -> { templateDictionaries.parallelStream()
addNewEntries(dictionaryModel, fromVersion.getDossierTemplateVersion(), newValues); .forEach(dictionaryModel -> addNewEntries(dictionaryModel, fromVersion.getDossierTemplateVersion(), newValues));
});
if (dossierDictionaryExists(dossierId)) { if (dossierDictionaryExists(dossierId)) {
List<DictionaryModel> dossierDictionaries = dictionaryCacheService.getDossierDictionary(dossierId).getDictionary(); List<DictionaryModel> dossierDictionaries = dictionaryCacheService.getDossierDictionary(dossierId).getDictionary();
dossierDictionaries.forEach(dictionaryModel -> { dossierDictionaries.parallelStream()
addNewEntries(dictionaryModel, fromVersion.getDossierVersion(), newValues); .forEach(dictionaryModel -> addNewEntries(dictionaryModel, fromVersion.getDossierVersion(), newValues));
});
} }
return new DictionaryIncrement(newValues, latestVersion); return new DictionaryIncrement(newValues, latestVersion);
@ -247,76 +246,103 @@ public class DictionaryService {
Set<DictionaryEntryModel> combinedFalseRecommendations) { Set<DictionaryEntryModel> combinedFalseRecommendations) {
if (oldModel.isCaseInsensitive() && !newType.isCaseInsensitive()) { if (oldModel.isCaseInsensitive() && !newType.isCaseInsensitive()) {
// Compute new entries' values in lowercase once
Set<String> newEntryValuesLower = newEntries.getEntries()
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet());
combinedEntries.addAll(oldModel.getEntries() combinedEntries.addAll(oldModel.getEntries()
.stream() .stream()
.filter(f -> !newEntries.getEntries() .filter(f -> !newEntryValuesLower.contains(f.getValue()))
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()).contains(f.getValue()))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
// Similarly for false positives
Set<String> newFalsePositivesValuesLower = newEntries.getFalsePositives()
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet());
combinedFalsePositives.addAll(oldModel.getFalsePositives() combinedFalsePositives.addAll(oldModel.getFalsePositives()
.stream() .stream()
.filter(f -> !newEntries.getFalsePositives() .filter(f -> !newFalsePositivesValuesLower.contains(f.getValue()))
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()).contains(f.getValue()))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
// Similarly for false recommendations
Set<String> newFalseRecommendationsValuesLower = newEntries.getFalseRecommendations()
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet());
combinedFalseRecommendations.addAll(oldModel.getFalseRecommendations() combinedFalseRecommendations.addAll(oldModel.getFalseRecommendations()
.stream() .stream()
.filter(f -> !newEntries.getFalseRecommendations() .filter(f -> !newFalseRecommendationsValuesLower.contains(f.getValue()))
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()).contains(f.getValue()))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} else if (!oldModel.isCaseInsensitive() && newType.isCaseInsensitive()) { } else if (!oldModel.isCaseInsensitive() && newType.isCaseInsensitive()) {
// Compute new entries' values in lowercase once
Set<String> newEntryValuesLower = newEntries.getEntries()
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet());
combinedEntries.addAll(oldModel.getEntries() combinedEntries.addAll(oldModel.getEntries()
.stream() .stream()
.filter(f -> !newEntries.getEntries() .filter(f -> !newEntryValuesLower.contains(f.getValue().toLowerCase(Locale.ROOT)))
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()).contains(f.getValue().toLowerCase(Locale.ROOT)))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
// Similarly for false positives
Set<String> newFalsePositivesValuesLower = newEntries.getFalsePositives()
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet());
combinedFalsePositives.addAll(oldModel.getFalsePositives() combinedFalsePositives.addAll(oldModel.getFalsePositives()
.stream() .stream()
.filter(f -> !newEntries.getFalsePositives() .filter(f -> !newFalsePositivesValuesLower.contains(f.getValue().toLowerCase(Locale.ROOT)))
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()).contains(f.getValue().toLowerCase(Locale.ROOT)))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
// Similarly for false recommendations
Set<String> newFalseRecommendationsValuesLower = newEntries.getFalseRecommendations()
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet());
combinedFalseRecommendations.addAll(oldModel.getFalseRecommendations() combinedFalseRecommendations.addAll(oldModel.getFalseRecommendations()
.stream() .stream()
.filter(f -> !newEntries.getFalseRecommendations() .filter(f -> !newFalseRecommendationsValuesLower.contains(f.getValue().toLowerCase(Locale.ROOT)))
.stream()
.map(s -> s.getValue().toLowerCase(Locale.ROOT))
.collect(Collectors.toSet()).contains(f.getValue().toLowerCase(Locale.ROOT)))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} else { } else {
// Both have the same case sensitivity
Set<String> newEntryValues = newEntries.getEntries()
.stream()
.map(DictionaryEntryModel::getValue)
.collect(Collectors.toSet());
combinedEntries.addAll(oldModel.getEntries() combinedEntries.addAll(oldModel.getEntries()
.stream() .stream()
.filter(f -> !newEntries.getEntries() .filter(f -> !newEntryValues.contains(f.getValue()))
.stream()
.map(DictionaryEntryModel::getValue)
.collect(Collectors.toSet()).contains(f.getValue()))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
// Similarly for false positives
Set<String> newFalsePositivesValues = newEntries.getFalsePositives()
.stream()
.map(DictionaryEntryModel::getValue)
.collect(Collectors.toSet());
combinedFalsePositives.addAll(oldModel.getFalsePositives() combinedFalsePositives.addAll(oldModel.getFalsePositives()
.stream() .stream()
.filter(f -> !newEntries.getFalsePositives() .filter(f -> !newFalsePositivesValues.contains(f.getValue()))
.stream()
.map(DictionaryEntryModel::getValue)
.collect(Collectors.toSet()).contains(f.getValue()))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
// Similarly for false recommendations
Set<String> newFalseRecommendationsValues = newEntries.getFalseRecommendations()
.stream()
.map(DictionaryEntryModel::getValue)
.collect(Collectors.toSet());
combinedFalseRecommendations.addAll(oldModel.getFalseRecommendations() combinedFalseRecommendations.addAll(oldModel.getFalseRecommendations()
.stream() .stream()
.filter(f -> !newEntries.getFalseRecommendations() .filter(f -> !newFalseRecommendationsValues.contains(f.getValue()))
.stream()
.map(DictionaryEntryModel::getValue)
.collect(Collectors.toSet()).contains(f.getValue()))
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
} }
@ -407,8 +433,8 @@ public class DictionaryService {
} }
return dictionaryFactory.create(mergedDictionaries.stream() return dictionaryFactory.create(mergedDictionaries.stream()
.sorted(Comparator.comparingInt(DictionaryModel::getRank).reversed()) .sorted(Comparator.comparingInt(DictionaryModel::getRank).reversed())
.collect(Collectors.toList()), dictionaryVersion); .collect(Collectors.toList()), dictionaryVersion);
} }

View File

@ -2,64 +2,97 @@ package com.iqser.red.service.redaction.v1.server.document.graph;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import java.security.SecureRandom; import java.io.BufferedReader;
import java.util.ArrayList; import java.io.InputStreamReader;
import java.util.HashMap; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import com.hankcs.algorithm.AhoCorasickDoubleArrayTrie; import com.hankcs.algorithm.AhoCorasickDoubleArrayTrie;
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryIdentifier; import com.iqser.red.service.redaction.v1.server.model.dictionary.*;
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionarySearch;
import com.iqser.red.service.redaction.v1.server.model.dictionary.DoubleArrayTrieDictionarySearch;
import com.iqser.red.service.redaction.v1.server.model.dictionary.TrieDictionarySearch;
import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange; import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;
public class DictionarySearchImplementationsTest { public class DictionarySearchImplementationsTest {
private static final int LARGE_DICTIONARY_SIZE = 4_000; private static final int LARGE_TEXT_REPETITIONS = 50_000;
private static final int LARGE_TEXT_REPETITIONS = 500_000; private static final int MAX_PII_ENTRY_COUNT = 500_000;
private static final String LARGE_TEXT_SAMPLE = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " private static final String LARGE_TEXT_SAMPLE = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+ "Entity_1 match text. Recommendation_1 also here. Random text continues. "; + "Entity_1 match text. Recommendation_1 also here. Random text continues. ";
// Dictionary identifiers
protected static final String VERTEBRATE_INDICATOR = "vertebrate";
protected static final String DICTIONARY_ADDRESS = "CBI_address";
protected static final String DICTIONARY_AUTHOR = "CBI_author";
protected static final String DICTIONARY_SPONSOR = "CBI_sponsor";
protected static final String DICTIONARY_PII = "PII";
protected static final String NO_REDACTION_INDICATOR = "no_redaction_indicator";
protected static final String REDACTION_INDICATOR = "redaction_indicator";
protected static final String HINT_ONLY_INDICATOR = "hint_only";
protected static final String MUST_REDACT_INDICATOR = "must_redact";
protected static final String PUBLISHED_INFORMATION_INDICATOR = "published_information";
protected static final String TEST_METHOD_INDICATOR = "test_method";
protected static final String PURITY_INDICATOR = "purity";
@Test @Test
@Disabled public void performanceTestWithRealDictionaries() {
public void performanceTest() {
// Load dictionaries from files
Map<String, List<String>> loadedDictionaries = loadDictionaries();
Map<DictionaryIdentifier, List<String>> dictionaryValues = new HashMap<>(); Map<DictionaryIdentifier, List<String>> dictionaryValues = new HashMap<>();
Random random = new Random();
// Generate dictionary values with specific terms for matching // Randomly assign EntityType.ENTITY or EntityType.RECOMMENDATION to dictionaries
IntStream.range(0, 6) for (Map.Entry<String, List<String>> entry : loadedDictionaries.entrySet()) {
.forEach(i -> { String dictionaryName = entry.getKey();
EntityType entityType = i % 2 == 0 ? EntityType.ENTITY : EntityType.RECOMMENDATION; List<String> dictionaryTerms = entry.getValue();
boolean caseSensitive = i % 2 == 0;
DictionaryIdentifier identifier = new DictionaryIdentifier("Type_" + i, entityType, true, caseSensitive); EntityType entityType = random.nextBoolean() ? EntityType.ENTITY : EntityType.RECOMMENDATION;
List<String> dictionary = generateLargeDictionary(); boolean caseSensitive = random.nextBoolean();
// Add specific terms that are included in the large text for matches DictionaryIdentifier identifier = new DictionaryIdentifier(
if (i == 0) { dictionaryName,
dictionary.add("Entity_1"); entityType,
} true,
if (i == 1) { caseSensitive
dictionary.add("recommendation_1"); );
}
dictionaryValues.put(identifier, dictionary); dictionaryValues.put(identifier, dictionaryTerms);
}); }
// **Added dummy dictionaries as per request**
// Case-sensitive dictionary containing "Entity_1"
DictionaryIdentifier entity1Identifier = new DictionaryIdentifier(
"dummy_case_sensitive",
EntityType.ENTITY,
true,
true // Case-sensitive
);
dictionaryValues.put(entity1Identifier, List.of("Entity_1"));
// Case-insensitive dictionary containing "recommendation_1"
DictionaryIdentifier recommendation1Identifier = new DictionaryIdentifier(
"dummy_case_insensitive",
EntityType.RECOMMENDATION,
true,
false // Case-insensitive
);
dictionaryValues.put(recommendation1Identifier, List.of("recommendation_1"));
// Measure construction time for TrieDictionarySearch // Measure construction time for TrieDictionarySearch
long trieDictionaryConstructionStart = System.currentTimeMillis(); long trieDictionaryConstructionStart = System.currentTimeMillis();
TrieDictionarySearch trieDictionarySearchImpl = new TrieDictionarySearch(dictionaryValues); DoubleTrieDictionarySearch doubleTrieDictionarySearchImpl = new DoubleTrieDictionarySearch(dictionaryValues);
long trieDictionaryConstructionDuration = System.currentTimeMillis() - trieDictionaryConstructionStart; long trieDictionaryConstructionDuration = System.currentTimeMillis() - trieDictionaryConstructionStart;
// Measure construction time for AnotherTrieDictionarySearch
long anotherTrieConstructionStart = System.currentTimeMillis();
Map<String, List<DictionaryIdentifierWithKeyword>> keyWordToIdentifiersMap = computeStringIdentifiersMap(dictionaryValues);
AhoCorasickMapDictionarySearch ahoCorasickMapDictionarySearchImpl = new AhoCorasickMapDictionarySearch(keyWordToIdentifiersMap);
long anotherTrieConstructionDuration = System.currentTimeMillis() - anotherTrieConstructionStart;
// Measure construction time for SearchImplementations // Measure construction time for SearchImplementations
long searchTrieConstructionStart = System.currentTimeMillis(); long searchTrieConstructionStart = System.currentTimeMillis();
List<SearchImplementation> searchImplementations = dictionaryValues.entrySet() List<SearchImplementation> searchImplementations = dictionaryValues.entrySet()
@ -70,16 +103,22 @@ public class DictionarySearchImplementationsTest {
// Measure construction time for DoubleArrayTrieDictionarySearch // Measure construction time for DoubleArrayTrieDictionarySearch
long doubleArrayTrieConstructionStart = System.currentTimeMillis(); long doubleArrayTrieConstructionStart = System.currentTimeMillis();
DoubleArrayTrieDictionarySearch doubleArrayTrieSearchImpl = new DoubleArrayTrieDictionarySearch(dictionaryValues); DoubleArrayTrieDictionarySearch doubleArrayTrieSearchImpl = new DoubleArrayTrieDictionarySearch(keyWordToIdentifiersMap);
long doubleArrayTrieConstructionDuration = System.currentTimeMillis() - doubleArrayTrieConstructionStart; long doubleArrayTrieConstructionDuration = System.currentTimeMillis() - doubleArrayTrieConstructionStart;
String largeText = LARGE_TEXT_SAMPLE.repeat(LARGE_TEXT_REPETITIONS); String largeText = LARGE_TEXT_SAMPLE.repeat(LARGE_TEXT_REPETITIONS);
// Measure search time for TrieDictionarySearch // Measure search time for TrieDictionarySearch
long trieDictionarySearchStart = System.currentTimeMillis(); long trieDictionarySearchStart = System.currentTimeMillis();
List<TrieDictionarySearch.MatchTextRange> trieDictionaryMatches = trieDictionarySearchImpl.getBoundariesAsList(largeText); List<DoubleTrieDictionarySearch.MatchTextRange> trieDictionaryMatches = doubleTrieDictionarySearchImpl.getBoundariesAsList(largeText);
long trieDictionarySearchDuration = System.currentTimeMillis() - trieDictionarySearchStart; long trieDictionarySearchDuration = System.currentTimeMillis() - trieDictionarySearchStart;
// Measure search time for AnotherTrieDictionarySearch
long anotherTrieSearchStart = System.currentTimeMillis();
List<DictionarySearch.MatchTextRange> anotherTrieMatches = ahoCorasickMapDictionarySearchImpl.getBoundaries(largeText).toList();
long anotherTrieSearchDuration = System.currentTimeMillis() - anotherTrieSearchStart;
// Measure search time for SearchImplementations // Measure search time for SearchImplementations
long searchImplStart = System.currentTimeMillis(); long searchImplStart = System.currentTimeMillis();
List<TextRange> searchMatches = new ArrayList<>(); List<TextRange> searchMatches = new ArrayList<>();
@ -94,72 +133,113 @@ public class DictionarySearchImplementationsTest {
long doubleArrayTrieSearchDuration = System.currentTimeMillis() - doubleArrayTrieSearchStart; long doubleArrayTrieSearchDuration = System.currentTimeMillis() - doubleArrayTrieSearchStart;
// Output the performance results // Output the performance results
System.out.printf("TrieDictionarySearch construction took %d ms\n", trieDictionaryConstructionDuration); System.out.println("\nTotal number of keywords is: " + keyWordToIdentifiersMap.size());
System.out.printf("TrieDictionarySearch search took %d ms and found %d matches\n", trieDictionarySearchDuration, trieDictionaryMatches.size());
System.out.printf("Combined Trie construction took %d ms\n", searchTrieConstructionDuration); System.out.printf("DoubleTrieDictionarySearch construction took %d ms%n", trieDictionaryConstructionDuration);
System.out.printf("Combined SearchImplementation search took %d ms and found %d matches\n", searchImplDuration, searchMatches.size()); System.out.printf("DoubleTrieDictionarySearch search took %d ms and found %d matches%n", trieDictionarySearchDuration, trieDictionaryMatches.size());
System.out.printf("DoubleArrayTrieDictionarySearch construction took %d ms\n", doubleArrayTrieConstructionDuration); System.out.println();
System.out.printf("DoubleArrayTrieDictionarySearch search took %d ms and found %d matches\n", doubleArrayTrieSearchDuration, doubleArrayTrieMatches.size());
System.out.printf("AhoCorasickMapDictionarySearch construction took %d ms%n", anotherTrieConstructionDuration);
System.out.printf("AhoCorasickMapDictionarySearch search took %d ms and found %d matches%n", anotherTrieSearchDuration, anotherTrieMatches.size());
System.out.println();
System.out.printf("Multiple Tries construction took %d ms%n", searchTrieConstructionDuration);
System.out.printf("Combined SearchImplementations search took %d ms and found %d matches%n", searchImplDuration, searchMatches.size());
System.out.println();
System.out.printf("DoubleArrayTrieDictionarySearch construction took %d ms%n", doubleArrayTrieConstructionDuration);
System.out.printf("DoubleArrayTrieDictionarySearch search took %d ms and found %d matches%n", doubleArrayTrieSearchDuration, doubleArrayTrieMatches.size());
System.out.println();
// Assert that all implementations found matches // Assert that all implementations found matches
assert !trieDictionaryMatches.isEmpty() && !searchMatches.isEmpty() && !doubleArrayTrieMatches.isEmpty() : "All implementations should find entities."; assert !trieDictionaryMatches.isEmpty() && !anotherTrieMatches.isEmpty() && !searchMatches.isEmpty() && !doubleArrayTrieMatches.isEmpty(): "All implementations should find entities.";
// Ensure all implementations found the same number of matches // Ensure all implementations found the same number of matches
assertEquals(trieDictionaryMatches.size(), searchMatches.size(), "Mismatch between TrieDictionarySearch and SearchImplementations"); int expectedMatches = trieDictionaryMatches.size();
assertEquals(trieDictionaryMatches.size(), doubleArrayTrieMatches.size(), "Mismatch between TrieDictionarySearch and DoubleArrayTrieDictionarySearch"); assertEquals(expectedMatches, anotherTrieMatches.size(), "Mismatch between DoubleTrieDictionarySearch and AhoCorasickMapDictionarySearch");
assertEquals(expectedMatches, searchMatches.size(), "Mismatch between DoubleTrieDictionarySearch and Combined SearchImplementations");
assertEquals(expectedMatches, doubleArrayTrieMatches.size(), "Mismatch between DoubleTrieDictionarySearch and DoubleArrayTrieDictionarySearch");
} }
private Map<String, List<String>> loadDictionaries() {
Map<String, List<String>> dictionaries = new HashMap<>();
private List<String> generateLargeDictionary() { dictionaries.put(DICTIONARY_AUTHOR, loadDictionaryFromFile("dictionaries/CBI_author.txt"));
dictionaries.put(DICTIONARY_SPONSOR, loadDictionaryFromFile("dictionaries/CBI_sponsor.txt"));
dictionaries.put(VERTEBRATE_INDICATOR, loadDictionaryFromFile("dictionaries/vertebrate.txt"));
dictionaries.put(DICTIONARY_ADDRESS, loadDictionaryFromFile("dictionaries/CBI_address.txt"));
dictionaries.put(NO_REDACTION_INDICATOR, loadDictionaryFromFile("dictionaries/no_redaction_indicator.txt"));
dictionaries.put(REDACTION_INDICATOR, loadDictionaryFromFile("dictionaries/redaction_indicator.txt"));
dictionaries.put(HINT_ONLY_INDICATOR, loadDictionaryFromFile("dictionaries/hint_only.txt"));
dictionaries.put(MUST_REDACT_INDICATOR, loadDictionaryFromFile("dictionaries/must_redact.txt"));
dictionaries.put(PUBLISHED_INFORMATION_INDICATOR, loadDictionaryFromFile("dictionaries/published_information.txt"));
dictionaries.put(TEST_METHOD_INDICATOR, loadDictionaryFromFile("dictionaries/test_method.txt"));
List<String> piis = loadDictionaryFromFile("dictionaries/PII_large.txt");
dictionaries.put(DICTIONARY_PII, MAX_PII_ENTRY_COUNT < piis.size() ? piis.subList(0, MAX_PII_ENTRY_COUNT) : piis);
dictionaries.put(PURITY_INDICATOR, loadDictionaryFromFile("dictionaries/purity.txt"));
return IntStream.range(0, LARGE_DICTIONARY_SIZE).mapToObj(i -> RandomStringGenerator.generateRandomString()) return dictionaries;
.collect(Collectors.toList());
} }
private List<String> loadDictionaryFromFile(String filePath) {
List<String> terms = new ArrayList<>();
static final class RandomStringGenerator { try (BufferedReader reader = new BufferedReader(new InputStreamReader(
Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(filePath))))) {
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; terms = reader.lines()
private static final int STRING_LENGTH = 50; .map(this::cleanDictionaryEntry)
private static final SecureRandom RANDOM = new SecureRandom(); .collect(Collectors.toList());
} catch (Exception e) {
public static String generateRandomString() { System.err.println("Failed to load dictionary from " + filePath + ": " + e.getMessage());
StringBuilder sb = new StringBuilder(STRING_LENGTH);
for (int i = 0; i < STRING_LENGTH; i++) {
int index = RANDOM.nextInt(CHARACTERS.length());
sb.append(CHARACTERS.charAt(index));
}
return sb.toString();
} }
return terms;
} }
private String cleanDictionaryEntry(String entry) {
return entry.trim();
}
private static Map<String, List<DictionaryIdentifierWithKeyword>> computeStringIdentifiersMap(Map<DictionaryIdentifier, List<String>> dictionaryValues) {
Map<String, List<DictionaryIdentifierWithKeyword>> stringToIdentifiersMap = new HashMap<>();
for (Map.Entry<DictionaryIdentifier, List<String>> entry : dictionaryValues.entrySet()) {
DictionaryIdentifier identifier = entry.getKey();
List<String> values = entry.getValue();
for (String value : values) {
DictionaryIdentifierWithKeyword idWithKeyword = new DictionaryIdentifierWithKeyword(identifier, value);
stringToIdentifiersMap.computeIfAbsent(value.toLowerCase(Locale.ROOT), k -> new ArrayList<>()).add(idWithKeyword);
}
}
return stringToIdentifiersMap;
}
@Test @Test
public void testMultiplePayloads() { public void testMultiplePayloads() {
TrieDictionarySearch dictionarySearchImpl = new TrieDictionarySearch(Map.of(new DictionaryIdentifier("type1", EntityType.ENTITY, false, false), DoubleTrieDictionarySearch dictionarySearchImpl = new DoubleTrieDictionarySearch(Map.of(
List.of("apple", "banana"), new DictionaryIdentifier("type1", EntityType.ENTITY, false, false),
new DictionaryIdentifier("type2", EntityType.RECOMMENDATION, false, false), List.of("apple", "banana"),
List.of("apple", "orange"), new DictionaryIdentifier("type2", EntityType.RECOMMENDATION, false, false),
new DictionaryIdentifier("type3", EntityType.FALSE_POSITIVE, false, false), List.of("apple", "orange"),
List.of("apple", "kiwi"))); new DictionaryIdentifier("type3", EntityType.FALSE_POSITIVE, false, false),
List.of("apple", "kiwi")));
List<TrieDictionarySearch.MatchTextRange> dictionaryMatches = dictionarySearchImpl.getBoundariesAsList( List<DoubleTrieDictionarySearch.MatchTextRange> dictionaryMatches = dictionarySearchImpl.getBoundariesAsList(
"an apple is delicious, a banana and a kiwi as well. orange is a color."); "an apple is delicious, a banana and a kiwi as well. orange is a color.");
assertEquals(dictionaryMatches.size(), 6); assertEquals(dictionaryMatches.size(), 6);
} }
@Test @Test
public void testDoubleArrayTrie() { public void testDoubleArrayTrie() {
Map<String, List<String>> map = new HashMap<>(); Map<String, List<String>> map = new HashMap<>();
String[] keyArray = new String[]{"hers", "his", "she", "he"}; String[] keyArray = new String[] { "hers", "his", "she", "he" };
for (String key : keyArray) { for (String key : keyArray) {
map.put(key, List.of(key, key, key)); map.put(key, List.of(key, key, key));
} }

View File

@ -29,7 +29,6 @@ import com.iqser.red.service.redaction.v1.server.logger.Context;
import com.iqser.red.service.redaction.v1.server.model.NerEntities; import com.iqser.red.service.redaction.v1.server.model.NerEntities;
import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary; import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;
import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionarySearch; import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionarySearch;
import com.iqser.red.service.redaction.v1.server.model.dictionary.TrieDictionarySearch;
import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;