11package datawave .query .transformer .annotation ;
22
3+ import static datawave .query .QueryParameters .INCLUDE_GROUPING_CONTEXT ;
4+ import static datawave .query .QueryParameters .RETURN_FIELDS ;
5+
36import java .net .URLDecoder ;
47import java .nio .charset .StandardCharsets ;
58import java .util .ArrayDeque ;
69import java .util .ArrayList ;
10+ import java .util .Arrays ;
711import java .util .HashMap ;
812import java .util .HashSet ;
913import java .util .Iterator ;
1014import java .util .List ;
1115import java .util .Map ;
1216import java .util .Map .Entry ;
1317import java .util .Objects ;
18+ import java .util .Optional ;
1419import java .util .Set ;
1520import java .util .TreeMap ;
1621import java .util .concurrent .TimeUnit ;
2126
2227import org .apache .accumulo .core .data .Key ;
2328import org .apache .commons .jexl3 .parser .ParseException ;
29+ import org .apache .commons .lang .StringUtils ;
2430import org .apache .log4j .Logger ;
2531
2632import com .fasterxml .jackson .core .JsonProcessingException ;
2733import com .fasterxml .jackson .core .type .TypeReference ;
2834import com .fasterxml .jackson .databind .ObjectMapper ;
35+ import com .google .common .collect .Sets ;
2936
3037import datawave .annotation .data .v1 .AnnotationDataAccess ;
3138import datawave .annotation .protobuf .v1 .Annotation ;
39+ import datawave .annotation .protobuf .v1 .AnnotationSource ;
3240import datawave .annotation .protobuf .v1 .Segment ;
3341import datawave .annotation .protobuf .v1 .SegmentBoundary ;
3442import datawave .annotation .protobuf .v1 .SegmentValue ;
3543import datawave .data .normalizer .Normalizer ;
3644import datawave .marking .MarkingFunctions ;
3745import datawave .microservice .query .Query ;
3846import datawave .query .attributes .Attribute ;
47+ import datawave .query .attributes .Attributes ;
3948import datawave .query .attributes .Content ;
4049import datawave .query .attributes .Document ;
50+ import datawave .query .config .ShardQueryConfiguration ;
51+ import datawave .query .function .RemoveGroupingContext ;
52+ import datawave .query .jexl .JexlASTHelper ;
4153import datawave .query .parser .JavaRegexAnalyzer ;
4254import datawave .query .transformer .DocumentTransform ;
4355import 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