Skip to content

Commit 0f38b23

Browse files
authored
Support enriching an annotation hit from the event (#3416)
closes #3410
1 parent b1c753e commit 0f38b23

8 files changed

Lines changed: 847 additions & 208 deletions

File tree

warehouse/query-core/src/main/java/datawave/query/config/annotation/AllHitsQueryConfig.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package datawave.query.config.annotation;
22

33
import java.io.Serializable;
4+
import java.util.HashMap;
5+
import java.util.Map;
46
import java.util.Objects;
57
import java.util.Set;
68

@@ -36,6 +38,11 @@ public class AllHitsQueryConfig implements Serializable {
3638

3739
private Normalizer<String> termNormalizer;
3840

41+
/**
42+
* A mapping between fields in the event associating an annotation to a field to be returned in an AllHits dynamic field.
43+
*/
44+
private Map<String,String> annotationEnrichmentFieldMap = new HashMap<>();
45+
3946
private AnnotationConfig annotationConfig;
4047

4148
public AllHitsQueryConfig() {
@@ -50,6 +57,7 @@ public AllHitsQueryConfig(AllHitsQueryConfig other) {
5057
setTargetField(other.getTargetField());
5158
setQueryTermExtractor(other.getQueryTermExtractor());
5259
setTermNormalizer(other.getTermNormalizer());
60+
setAnnotationEnrichmentFieldMap(other.getAnnotationEnrichmentFieldMap());
5361
setAnnotationConfig(other.getAnnotationConfig());
5462
}
5563

@@ -69,6 +77,7 @@ public boolean equals(Object other) {
6977
Objects.equals(getTargetField(), that.getTargetField()) &&
7078
Objects.equals(getQueryTermExtractor(), that.getQueryTermExtractor()) &&
7179
Objects.equals(getTermNormalizer(), that.getTermNormalizer()) &&
80+
Objects.equals(getAnnotationEnrichmentFieldMap(), that.getAnnotationEnrichmentFieldMap()) &&
7281
Objects.equals(getAnnotationConfig(), that.getAnnotationConfig());
7382
// @formatter:on
7483
}
@@ -84,6 +93,7 @@ public int hashCode() {
8493
getTargetField(),
8594
getQueryTermExtractor(),
8695
getTermNormalizer(),
96+
getAnnotationEnrichmentFieldMap(),
8797
getAnnotationConfig()
8898
);
8999
// @formatter:on
@@ -152,4 +162,12 @@ public Normalizer<String> getTermNormalizer() {
152162
public void setTermNormalizer(Normalizer<String> termNormalizer) {
153163
this.termNormalizer = termNormalizer;
154164
}
165+
166+
public Map<String,String> getAnnotationEnrichmentFieldMap() {
167+
return annotationEnrichmentFieldMap;
168+
}
169+
170+
public void setAnnotationEnrichmentFieldMap(Map<String,String> annotationEnrichmentFieldMap) {
171+
this.annotationEnrichmentFieldMap = annotationEnrichmentFieldMap;
172+
}
155173
}

warehouse/query-core/src/main/java/datawave/query/tables/ShardQueryLogic.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,14 +836,16 @@ private void addConfigBasedTransformers() throws QueryException {
836836
// since this may be called multiple times always rebuild
837837
// @formatter:off
838838
((DocumentTransformer) this.transformerInstance).addTransform(new AnnotationHitsTransformer(
839+
getConfig(),
839840
getConfig().getOriginalJexlQuery(),
840841
allHitsQueryConfig.getQueryTermExtractor(),
841842
allHitsQueryConfig.getTermNormalizer(),
842843
getAnnotationDataAccess(),
843844
getAnnotationHitsFactory(),
844845
allHitsQueryConfig.getMaxContextLength(),
845846
allHitsQueryConfig.getValidAnnotationTypes(),
846-
allHitsQueryConfig.getTargetField()));
847+
allHitsQueryConfig.getTargetField(),
848+
allHitsQueryConfig.getAnnotationEnrichmentFieldMap()));
847849
// @formatter:on
848850
}
849851

warehouse/query-core/src/main/java/datawave/query/transformer/annotation/AnnotationHitsTransformer.java

Lines changed: 189 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package datawave.query.transformer.annotation;
22

3+
import static datawave.query.QueryParameters.INCLUDE_GROUPING_CONTEXT;
4+
import static datawave.query.QueryParameters.RETURN_FIELDS;
5+
36
import java.net.URLDecoder;
47
import java.nio.charset.StandardCharsets;
58
import java.util.ArrayDeque;
69
import java.util.ArrayList;
10+
import java.util.Arrays;
711
import java.util.HashMap;
812
import java.util.HashSet;
913
import java.util.Iterator;
1014
import java.util.List;
1115
import java.util.Map;
1216
import java.util.Map.Entry;
1317
import java.util.Objects;
18+
import java.util.Optional;
1419
import java.util.Set;
1520
import java.util.TreeMap;
1621
import java.util.concurrent.TimeUnit;
@@ -21,27 +26,34 @@
2126

2227
import org.apache.accumulo.core.data.Key;
2328
import org.apache.commons.jexl3.parser.ParseException;
29+
import org.apache.commons.lang.StringUtils;
2430
import org.apache.log4j.Logger;
2531

2632
import com.fasterxml.jackson.core.JsonProcessingException;
2733
import com.fasterxml.jackson.core.type.TypeReference;
2834
import com.fasterxml.jackson.databind.ObjectMapper;
35+
import com.google.common.collect.Sets;
2936

3037
import datawave.annotation.data.v1.AnnotationDataAccess;
3138
import datawave.annotation.protobuf.v1.Annotation;
39+
import datawave.annotation.protobuf.v1.AnnotationSource;
3240
import datawave.annotation.protobuf.v1.Segment;
3341
import datawave.annotation.protobuf.v1.SegmentBoundary;
3442
import datawave.annotation.protobuf.v1.SegmentValue;
3543
import datawave.data.normalizer.Normalizer;
3644
import datawave.marking.MarkingFunctions;
3745
import datawave.microservice.query.Query;
3846
import datawave.query.attributes.Attribute;
47+
import datawave.query.attributes.Attributes;
3948
import datawave.query.attributes.Content;
4049
import datawave.query.attributes.Document;
50+
import datawave.query.config.ShardQueryConfiguration;
51+
import datawave.query.function.RemoveGroupingContext;
52+
import datawave.query.jexl.JexlASTHelper;
4153
import datawave.query.parser.JavaRegexAnalyzer;
4254
import datawave.query.transformer.DocumentTransform;
4355
import datawave.query.transformer.annotation.model.AllHits;
44-
import datawave.query.transformer.annotation.model.AllHitsError;
56+
import datawave.query.util.Tuple2;
4557

4658
/**
4759
* This Transform will lookup and search annotations for hits as well as provide context
@@ -63,6 +75,12 @@ public class AnnotationHitsTransformer extends DocumentTransform.DefaultDocument
6375
private static final SegmentValueByScoreComparator SEGMENT_VALUE_BY_SCORE_COMPARATOR = new SegmentValueByScoreComparator();
6476
private static final BoundaryComparator BOUNDARY_COMPARATOR = new BoundaryComparator();
6577

78+
/**
79+
* Depending on how the query is configured to run by the user, adjustments may need to be made to forcibly enable grouping notation and return fields so
80+
* that this transformer will have all the data necessary to fully enrichment responses. These changes will be made transparently to the user, and removed
81+
* before the final Document is returned from the transformer. This is only necessary if enrichmentFieldMap is populated.
82+
*/
83+
private final ShardQueryConfiguration shardQueryConfig;
6684
private final AnnotationDataAccess annotationDataAccess;
6785
private final AllHitsFactory allHitsFactory;
6886
private final int maxContextBoundary;
@@ -71,18 +89,27 @@ public class AnnotationHitsTransformer extends DocumentTransform.DefaultDocument
7189
private final TermExtractor queryTermExtractor;
7290
private final Normalizer<String> termNormalizer;
7391
private final String jexlQueryString;
92+
/**
93+
* Used for merging data about an annotation that may have been stored in the event with hits against that annotation. The key represents the field that
94+
* should be searched in the Document. The value is the name of the field to store in the AllHits dynamicFields when found. For each AllHits object that is
95+
* generated, check the Document for the presence of these fields and add them to the AllHits response.
96+
*/
97+
private final Map<String,String> enrichmentFieldMap;
7498

7599
private boolean enabled = DEFAULT_ENABLED;
76100
private int contextSize = DEFAULT_CONTEXT_SIZE;
77101
private float minScore = DEFAULT_MIN_SCORE;
78102
private TimeUnit timeUnit = DEFAULT_TIMEUNIT;
103+
private boolean forcedGroupingNotation = false;
104+
private List<String> forcedReturnFields = new ArrayList<>();
79105

80106
private Set<Pattern> searchHitTerms;
81107
private ObjectMapper objectMapper;
82108

83-
public AnnotationHitsTransformer(@Nullable String jexlQueryString, TermExtractor queryTermExtractor, Normalizer<String> termNormalizer,
84-
AnnotationDataAccess annotationDataAccess, AllHitsFactory allHitsFactory, int maxContextBoundary, Set<String> validTypes,
85-
String targetField) {
109+
public AnnotationHitsTransformer(ShardQueryConfiguration shardQueryConfig, @Nullable String jexlQueryString, TermExtractor queryTermExtractor,
110+
Normalizer<String> termNormalizer, AnnotationDataAccess annotationDataAccess, AllHitsFactory allHitsFactory, int maxContextBoundary,
111+
Set<String> validTypes, String targetField, Map<String,String> enrichmentFieldMap) {
112+
this.shardQueryConfig = shardQueryConfig;
86113
this.jexlQueryString = jexlQueryString;
87114
this.queryTermExtractor = queryTermExtractor;
88115
this.termNormalizer = termNormalizer;
@@ -91,6 +118,7 @@ public AnnotationHitsTransformer(@Nullable String jexlQueryString, TermExtractor
91118
this.maxContextBoundary = maxContextBoundary;
92119
this.validTypes = validTypes;
93120
this.targetField = targetField;
121+
this.enrichmentFieldMap = enrichmentFieldMap;
94122
}
95123

96124
@Override
@@ -165,6 +193,52 @@ public void initialize(Query settings, MarkingFunctions markingFunctions) {
165193
searchHitTerms.add(compileNormalized(termNormalizer.normalize(keyword)));
166194
}
167195
}
196+
197+
// test for changes that need to be made to the query to support field enrichment from the event
198+
if (enrichmentFieldMap != null && !enrichmentFieldMap.isEmpty()) {
199+
String groupingParameter = settings.findParameter(INCLUDE_GROUPING_CONTEXT).getParameterValue();
200+
if ((!Boolean.parseBoolean(groupingParameter))) {
201+
// grouping notation not set, apply it
202+
shardQueryConfig.setIncludeGroupingContext(true);
203+
// capture this, so it can be undone after the transform is complete
204+
forcedGroupingNotation = true;
205+
}
206+
207+
// now check that if return.fields are set that the fields required are included
208+
String returnFieldsParameter = settings.findParameter(RETURN_FIELDS).getParameterValue();
209+
if (returnFieldsParameter != null && !returnFieldsParameter.isBlank()) {
210+
// parse the return fields to get the list of missing return fields
211+
List<String> missingReturnFields = getMissingReturnFields(returnFieldsParameter);
212+
213+
if (!missingReturnFields.isEmpty()) {
214+
Set<String> updatedProjectFields = new HashSet<>(shardQueryConfig.getProjectFields());
215+
updatedProjectFields.addAll(missingReturnFields);
216+
shardQueryConfig.setProjectFields(updatedProjectFields);
217+
// capture this to undo what was forced into the query to satisfy the response after processing
218+
forcedReturnFields = missingReturnFields;
219+
}
220+
}
221+
}
222+
}
223+
224+
private List<String> getMissingReturnFields(String returnFieldsParameter) {
225+
String[] returnFields = returnFieldsParameter.split(",");
226+
// build a list of all the missing return fields that are in the enrichment map
227+
List<String> missingReturnFields = new ArrayList<>();
228+
for (String eventField : enrichmentFieldMap.keySet()) {
229+
boolean found = false;
230+
for (String returnField : returnFields) {
231+
if (eventField.equals(returnField)) {
232+
found = true;
233+
break;
234+
}
235+
}
236+
237+
if (!found) {
238+
missingReturnFields.add(eventField);
239+
}
240+
}
241+
return missingReturnFields;
168242
}
169243

170244
/**
@@ -243,20 +317,129 @@ public Entry<Key,Document> apply(@Nullable Entry<Key,Document> keyDocumentEntry)
243317
if (!orderedHits.isEmpty()) {
244318
results = allHitsFactory.create(annotation.getAnnotationId(), orderedHits, sortedSegments, timeUnit);
245319
}
320+
enrichAllHitsFromDocument(annotation, results, document);
246321
updateDocument(keyDocumentEntry, results);
247322
} catch (AllHitsException e) {
248323
log.warn("failed to process hit(s) on annotation: " + annotation.getAnnotationId() + " for doc: " + dataType + "\\x00" + uid, e);
249-
AllHitsError error = new AllHitsError();
324+
AllHits error = new AllHits();
250325
error.setAnnotationId(annotation.getAnnotationId());
251-
error.setErrorMessage(e.getMessage());
326+
error.addDynamicProperties("error", e.getMessage());
252327
updateDocument(keyDocumentEntry, error);
328+
} finally {
329+
// strip anything we forced into the Document to complete the query
330+
keyDocumentEntry = stripGroupingNotation(keyDocumentEntry);
331+
keyDocumentEntry = removeForcedFields(keyDocumentEntry);
253332
}
254333
}
255334
}
256335

257336
return keyDocumentEntry;
258337
}
259338

339+
private Entry<Key,Document> removeForcedFields(Entry<Key,Document> entry) {
340+
if (forcedReturnFields.isEmpty()) {
341+
return entry;
342+
}
343+
344+
Set<Tuple2<String,Attribute<? extends Comparable<?>>>> toRemove = Sets.newHashSet();
345+
for (Entry<String,Attribute<? extends Comparable<?>>> attribute : entry.getValue().entrySet()) {
346+
String fieldName = attribute.getKey();
347+
String baseFieldName = JexlASTHelper.deconstructIdentifier(fieldName);
348+
if (forcedReturnFields.contains(baseFieldName)) {
349+
toRemove.add(new Tuple2<>(attribute.getKey(), attribute.getValue()));
350+
}
351+
}
352+
353+
// remove everyone with a grouping context
354+
for (Tuple2<String,Attribute<? extends Comparable<?>>> goner : toRemove) {
355+
entry.getValue().removeAll(goner.first());
356+
}
357+
358+
return entry;
359+
}
360+
361+
private Entry<Key,Document> stripGroupingNotation(Entry<Key,Document> entry) {
362+
if (!forcedGroupingNotation) {
363+
return entry;
364+
}
365+
366+
RemoveGroupingContext removeGroupingContext = new RemoveGroupingContext();
367+
return removeGroupingContext.apply(entry);
368+
}
369+
370+
private void enrichAllHitsFromDocument(Annotation annotation, AllHits allHits, Document document) {
371+
if (allHits == null || enrichmentFieldMap.isEmpty()) {
372+
return;
373+
}
374+
375+
// this operation may cause an accumulo lookup
376+
Optional<AnnotationSource> optionalAnnotationSource = annotationDataAccess.getAnnotationSource(annotation.getAnalyticSourceHash());
377+
if (optionalAnnotationSource.isEmpty()) {
378+
log.info("could not enrich from event due to missing annotationSource for annotation:" + annotation.getAnnotationId() + " for doc:"
379+
+ annotation.getShard() + " " + annotation.getDataType() + " " + annotation.getUid());
380+
return;
381+
}
382+
383+
// this hash will match the beginning of grouping notation generated for related fields in the event
384+
String annotationSourceHash = optionalAnnotationSource.get().getAnalyticHash();
385+
386+
// since the document will be in grouping notation, there will not be an exact match on a field, iterate over the Document fields to find those that are
387+
// candidates
388+
for (String docField : document.getDictionary().keySet()) {
389+
String baseFieldName = JexlASTHelper.deconstructIdentifier(docField, false);
390+
if (!enrichmentFieldMap.containsKey(baseFieldName)) {
391+
// the base name isn't in the enrichment field map skip to the next one
392+
continue;
393+
}
394+
395+
String[] groups = docField.split("\\.");
396+
// all annotation fields will be in grouping notation of the form FIELD.annotationHash.segmentHash.valueHash
397+
if (groups.length != 4) {
398+
// doesn't match the required notation of the annotation enriched fields
399+
continue;
400+
}
401+
402+
// check the position of the analytic hash with the target value
403+
if (!groups[1].equals(annotationSourceHash)) {
404+
// doesn't match the annotation source hash from the annotation, not the target value
405+
continue;
406+
}
407+
408+
// get any pre-existing value in the map
409+
String[] existingSplits = null;
410+
String existing = allHits.getDynamicProperties().get(enrichmentFieldMap.get(baseFieldName));
411+
if (existing != null && !existing.isBlank()) {
412+
existingSplits = existing.split(";");
413+
}
414+
415+
// extract the value for enrichment
416+
Attribute<?> attr = document.get(docField);
417+
List<String> values = new ArrayList<>();
418+
if (existingSplits != null) {
419+
values.addAll(Arrays.asList(existingSplits));
420+
}
421+
boolean updated = false;
422+
if (attr instanceof Attributes) {
423+
// multi-valued
424+
Attributes attrs = (Attributes) attr;
425+
Set<Attribute<? extends Comparable<?>>> attrSet = attrs.getAttributes();
426+
427+
for (Attribute<? extends Comparable<?>> value : attrSet) {
428+
values.add(String.valueOf(value.getData()));
429+
}
430+
updated = true;
431+
} else if (attr != null) {
432+
// single value
433+
values.add(String.valueOf(attr.getData()));
434+
updated = true;
435+
}
436+
437+
if (updated) {
438+
allHits.addDynamicProperties(enrichmentFieldMap.get(baseFieldName), StringUtils.join(values, ";"));
439+
}
440+
}
441+
}
442+
260443
private void updateDocument(Entry<Key,Document> entry, @Nullable AllHits allHits) {
261444
List<AllHits> rollup = getCurrentAllHitsValue(entry);
262445
if (allHits != null) {

0 commit comments

Comments
 (0)