1313import org .apache .lucene .index .DocValues ;
1414import org .apache .lucene .index .DocValuesType ;
1515import org .apache .lucene .index .IndexOptions ;
16+ import org .apache .lucene .index .LeafReader ;
1617import org .apache .lucene .index .LeafReaderContext ;
1718import org .apache .lucene .index .SortedSetDocValues ;
1819import org .apache .lucene .index .TermsEnum ;
1920import org .apache .lucene .search .SortField ;
2021import org .apache .lucene .util .BytesRef ;
22+ import org .elasticsearch .common .bytes .BytesArray ;
2123import org .elasticsearch .common .io .stream .ByteArrayStreamInput ;
2224import org .elasticsearch .common .io .stream .BytesStreamOutput ;
2325import org .elasticsearch .common .util .BigArrays ;
3537import org .elasticsearch .index .mapper .MappedFieldType ;
3638import org .elasticsearch .index .mapper .Mapper ;
3739import org .elasticsearch .index .mapper .MapperBuilderContext ;
40+ import org .elasticsearch .index .mapper .SourceLoader ;
3841import org .elasticsearch .index .mapper .SourceValueFetcher ;
3942import org .elasticsearch .index .mapper .StringFieldType ;
4043import org .elasticsearch .index .mapper .TextSearchInfo ;
4649import org .elasticsearch .search .aggregations .support .CoreValuesSourceType ;
4750import org .elasticsearch .search .sort .BucketedSort ;
4851import org .elasticsearch .search .sort .SortOrder ;
52+ import org .elasticsearch .xcontent .XContentBuilder ;
4953import org .elasticsearch .xcontent .XContentParser ;
5054
5155import java .io .IOException ;
7276 * 2 for each key (one per document), a <code>counted_terms</code> aggregation on a <code>counted_keyword</code> field will consider
7377 * the actual count and report a count of 3 for each key.</p>
7478 *
75- * <p>Only regular source is supported; synthetic source won't work.</p>
79+ * <p>Synthetic source is supported, but uses the fallback "ignore source" infrastructure unless the <code>source_keep_mode</code> is
80+ * explicitly set to <code>none</code> in the field mapping parameters.</p>
7681 */
7782public class CountedKeywordFieldMapper extends FieldMapper {
7883 public static final String CONTENT_TYPE = "counted_keyword" ;
@@ -309,6 +314,81 @@ public FieldMapper build(MapperBuilderContext context) {
309314 }
310315 }
311316
317+ private static class CountedKeywordFieldSyntheticSourceLoader extends SourceLoader .DocValuesBasedSyntheticFieldLoader {
318+ private final String keywordsFieldName ;
319+ private final String countsFieldName ;
320+ private final String leafName ;
321+
322+ private SortedSetDocValues keywordsReader ;
323+ private BinaryDocValues countsReader ;
324+ private boolean hasValue ;
325+
326+ CountedKeywordFieldSyntheticSourceLoader (String keywordsFieldName , String countsFieldName , String leafName ) {
327+ this .keywordsFieldName = keywordsFieldName ;
328+ this .countsFieldName = countsFieldName ;
329+ this .leafName = leafName ;
330+ }
331+
332+ @ Override
333+ public DocValuesLoader docValuesLoader (LeafReader leafReader , int [] docIdsInLeaf ) throws IOException {
334+ keywordsReader = leafReader .getSortedSetDocValues (keywordsFieldName );
335+ countsReader = leafReader .getBinaryDocValues (countsFieldName );
336+
337+ if (keywordsReader == null || countsReader == null ) {
338+ return null ;
339+ }
340+
341+ return docId -> {
342+ hasValue = keywordsReader .advanceExact (docId );
343+ if (hasValue == false ) {
344+ return false ;
345+ }
346+
347+ boolean countsHasValue = countsReader .advanceExact (docId );
348+ assert countsHasValue ;
349+
350+ return true ;
351+ };
352+ }
353+
354+ @ Override
355+ public boolean hasValue () {
356+ return hasValue ;
357+ }
358+
359+ @ Override
360+ public void write (XContentBuilder b ) throws IOException {
361+ if (hasValue == false ) {
362+ return ;
363+ }
364+
365+ int [] counts = new BytesArray (countsReader .binaryValue ()).streamInput ().readVIntArray ();
366+ boolean singleValue = counts .length == 1 && counts [0 ] == 1 ;
367+
368+ if (singleValue ) {
369+ b .field (leafName );
370+ } else {
371+ b .startArray (leafName );
372+ }
373+
374+ for (int i = 0 ; i < keywordsReader .docValueCount (); i ++) {
375+ BytesRef currKeyword = keywordsReader .lookupOrd (keywordsReader .nextOrd ());
376+ for (int j = 0 ; j < counts [i ]; j ++) {
377+ b .utf8Value (currKeyword .bytes , currKeyword .offset , currKeyword .length );
378+ }
379+ }
380+
381+ if (singleValue == false ) {
382+ b .endArray ();
383+ }
384+ }
385+
386+ @ Override
387+ public String fieldName () {
388+ return keywordsFieldName ;
389+ }
390+ }
391+
312392 public static TypeParser PARSER = new TypeParser ((n , c ) -> new CountedKeywordFieldMapper .Builder (n ));
313393
314394 private final FieldType fieldType ;
@@ -345,6 +425,11 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
345425 } else {
346426 throw new IllegalArgumentException ("Encountered unexpected token [" + parser .currentToken () + "]." );
347427 }
428+
429+ if (values .isEmpty ()) {
430+ return ;
431+ }
432+
348433 int i = 0 ;
349434 int [] counts = new int [values .size ()];
350435 for (Map .Entry <String , Integer > value : values .entrySet ()) {
@@ -358,13 +443,18 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
358443
359444 private void parseArray (DocumentParserContext context , SortedMap <String , Integer > values ) throws IOException {
360445 XContentParser parser = context .parser ();
446+ int arrDepth = 1 ;
361447 while (true ) {
362448 XContentParser .Token token = parser .nextToken ();
363449 if (token == XContentParser .Token .END_ARRAY ) {
364- return ;
365- }
366- if (token == XContentParser .Token .VALUE_STRING ) {
450+ arrDepth -= 1 ;
451+ if (arrDepth <= 0 ) {
452+ return ;
453+ }
454+ } else if (token == XContentParser .Token .VALUE_STRING ) {
367455 parseValue (parser , values );
456+ } else if (token == XContentParser .Token .START_ARRAY ) {
457+ arrDepth += 1 ;
368458 } else if (token == XContentParser .Token .VALUE_NULL ) {
369459 // ignore null values
370460 } else {
@@ -402,4 +492,16 @@ public FieldMapper.Builder getMergeBuilder() {
402492 protected String contentType () {
403493 return CONTENT_TYPE ;
404494 }
495+
496+ @ Override
497+ protected SyntheticSourceSupport syntheticSourceSupport () {
498+ var keepMode = sourceKeepMode ();
499+ if (keepMode .isPresent () == false || keepMode .get () != SourceKeepMode .NONE ) {
500+ return super .syntheticSourceSupport ();
501+ }
502+
503+ var loader = new CountedKeywordFieldSyntheticSourceLoader (fullPath (), countFieldMapper .fullPath (), leafName ());
504+ return new SyntheticSourceSupport .Native (loader );
505+ }
506+
405507}
0 commit comments