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" ;
@@ -306,6 +311,81 @@ public FieldMapper build(MapperBuilderContext context) {
306311 }
307312 }
308313
314+ private static class CountedKeywordFieldSyntheticSourceLoader extends SourceLoader .DocValuesBasedSyntheticFieldLoader {
315+ private final String keywordsFieldName ;
316+ private final String countsFieldName ;
317+ private final String leafName ;
318+
319+ private SortedSetDocValues keywordsReader ;
320+ private BinaryDocValues countsReader ;
321+ private boolean hasValue ;
322+
323+ CountedKeywordFieldSyntheticSourceLoader (String keywordsFieldName , String countsFieldName , String leafName ) {
324+ this .keywordsFieldName = keywordsFieldName ;
325+ this .countsFieldName = countsFieldName ;
326+ this .leafName = leafName ;
327+ }
328+
329+ @ Override
330+ public DocValuesLoader docValuesLoader (LeafReader leafReader , int [] docIdsInLeaf ) throws IOException {
331+ keywordsReader = leafReader .getSortedSetDocValues (keywordsFieldName );
332+ countsReader = leafReader .getBinaryDocValues (countsFieldName );
333+
334+ if (keywordsReader == null || countsReader == null ) {
335+ return null ;
336+ }
337+
338+ return docId -> {
339+ hasValue = keywordsReader .advanceExact (docId );
340+ if (hasValue == false ) {
341+ return false ;
342+ }
343+
344+ boolean countsHasValue = countsReader .advanceExact (docId );
345+ assert countsHasValue ;
346+
347+ return true ;
348+ };
349+ }
350+
351+ @ Override
352+ public boolean hasValue () {
353+ return hasValue ;
354+ }
355+
356+ @ Override
357+ public void write (XContentBuilder b ) throws IOException {
358+ if (hasValue == false ) {
359+ return ;
360+ }
361+
362+ int [] counts = new BytesArray (countsReader .binaryValue ()).streamInput ().readVIntArray ();
363+ boolean singleValue = counts .length == 1 && counts [0 ] == 1 ;
364+
365+ if (singleValue ) {
366+ b .field (leafName );
367+ } else {
368+ b .startArray (leafName );
369+ }
370+
371+ for (int i = 0 ; i < keywordsReader .docValueCount (); i ++) {
372+ BytesRef currKeyword = keywordsReader .lookupOrd (keywordsReader .nextOrd ());
373+ for (int j = 0 ; j < counts [i ]; j ++) {
374+ b .utf8Value (currKeyword .bytes , currKeyword .offset , currKeyword .length );
375+ }
376+ }
377+
378+ if (singleValue == false ) {
379+ b .endArray ();
380+ }
381+ }
382+
383+ @ Override
384+ public String fieldName () {
385+ return keywordsFieldName ;
386+ }
387+ }
388+
309389 public static TypeParser PARSER = new TypeParser ((n , c ) -> new CountedKeywordFieldMapper .Builder (n ));
310390
311391 private final FieldType fieldType ;
@@ -342,6 +422,11 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
342422 } else {
343423 throw new IllegalArgumentException ("Encountered unexpected token [" + parser .currentToken () + "]." );
344424 }
425+
426+ if (values .isEmpty ()) {
427+ return ;
428+ }
429+
345430 int i = 0 ;
346431 int [] counts = new int [values .size ()];
347432 for (Map .Entry <String , Integer > value : values .entrySet ()) {
@@ -355,13 +440,18 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
355440
356441 private void parseArray (DocumentParserContext context , SortedMap <String , Integer > values ) throws IOException {
357442 XContentParser parser = context .parser ();
443+ int arrDepth = 1 ;
358444 while (true ) {
359445 XContentParser .Token token = parser .nextToken ();
360446 if (token == XContentParser .Token .END_ARRAY ) {
361- return ;
362- }
363- if (token == XContentParser .Token .VALUE_STRING ) {
447+ arrDepth -= 1 ;
448+ if (arrDepth <= 0 ) {
449+ return ;
450+ }
451+ } else if (token == XContentParser .Token .VALUE_STRING ) {
364452 parseValue (parser , values );
453+ } else if (token == XContentParser .Token .START_ARRAY ) {
454+ arrDepth += 1 ;
365455 } else if (token == XContentParser .Token .VALUE_NULL ) {
366456 // ignore null values
367457 } else {
@@ -399,4 +489,16 @@ public FieldMapper.Builder getMergeBuilder() {
399489 protected String contentType () {
400490 return CONTENT_TYPE ;
401491 }
492+
493+ @ Override
494+ protected SyntheticSourceSupport syntheticSourceSupport () {
495+ var keepMode = sourceKeepMode ();
496+ if (keepMode .isPresent () == false || keepMode .get () != SourceKeepMode .NONE ) {
497+ return super .syntheticSourceSupport ();
498+ }
499+
500+ var loader = new CountedKeywordFieldSyntheticSourceLoader (fullPath (), countFieldMapper .fullPath (), leafName ());
501+ return new SyntheticSourceSupport .Native (loader );
502+ }
503+
402504}
0 commit comments