static InvokerResult convertToResult(Query query, SearchProtocol.SearchReply protobuf,
                                         DocumentDatabase documentDatabase, int partId, int distKey)
    {
        InvokerResult result = new InvokerResult(query, protobuf.getHitsCount());

        result.getResult().setTotalHitCount(protobuf.getTotalHitCount());
        result.getResult().setCoverage(convertToCoverage(protobuf));

        convertSearchReplyErrors(result.getResult(), protobuf.getErrorsList());
        List<String> featureNames = protobuf.getMatchFeatureNamesList();
        var haveMatchFeatures = ! featureNames.isEmpty();
        MatchFeatureData matchFeatures = haveMatchFeatures ? new MatchFeatureData(featureNames) : null;
        var haveGrouping = ! protobuf.getGroupingBlob().isEmpty();
        if (haveGrouping) {
            BufferSerializer buf = new BufferSerializer(new GrowableByteBuffer(protobuf.getGroupingBlob().asReadOnlyByteBuffer()));
            int cnt = buf.getInt(null);
            ArrayList<Grouping> list = new ArrayList<>(cnt);
            for (int i = 0; i < cnt; i++) {
                Grouping g = new Grouping();
                g.deserialize(buf);
                list.add(g);
            }
            GroupingListHit hit = new GroupingListHit(list, documentDatabase, query);
            result.getResult().hits().add(hit);
        }
        for (var replyHit : protobuf.getHitsList()) {
            LeanHit hit = (replyHit.getSortData().isEmpty())
                    ? new LeanHit(replyHit.getGlobalId().toByteArray(), partId, distKey, replyHit.getRelevance())
                    : new LeanHit(replyHit.getGlobalId().toByteArray(), partId, distKey, replyHit.getRelevance(), replyHit.getSortData().toByteArray());
            if (haveMatchFeatures) {
                var hitFeatures = matchFeatures.addHit();
                var featureList = replyHit.getMatchFeaturesList();
                if (featureList.size() == featureNames.size()) {
                    int idx = 0;
                    for (SearchProtocol.Feature value : featureList) {
                        ByteString tensorBlob = value.getTensor();
                        if (tensorBlob.isEmpty()) {
                            hitFeatures.set(idx++, value.getNumber());
                        } else {
                            hitFeatures.set(idx++, tensorBlob.toByteArray());
                        }
                    }
                    hit.addMatchFeatures(hitFeatures);
                } else {
                    result.getResult().hits().addError(ErrorMessage.createBackendCommunicationError("mismatch in match feature sizes"));
                }
            }
            result.getLeanHits().add(hit);
        }

        var slimeTrace = protobuf.getSlimeTrace();
        if ( ! slimeTrace.isEmpty()) {
            var traces = new Value.ArrayValue();
            traces.add(new SlimeAdapter(BinaryFormat.decode(slimeTrace.toByteArray()).get()));
            query.trace(traces, query.getTrace().getLevel());
        }
        return result;
    }
@Test
    void testSearchReplyDecodingWithRelevance() {
        Query q = new Query("search/?query=test");
        InvokerResult result = ProtobufSerialization.convertToResult(q, createSearchReply(5, false), null, 1, 2);
        assertEquals(result.getResult().getTotalHitCount(), 7);
        List<LeanHit> hits = result.getLeanHits();
        assertEquals(5, hits.size());
        double expectedRelevance = 5;
        int hitNum = 0;
        for (LeanHit hit : hits) {
            assertEquals('a', hit.getGid()[0]);
            assertEquals(hitNum, hit.getGid()[11]);
            assertEquals(expectedRelevance--, hit.getRelevance(), DELTA);
            assertEquals(1, hit.getPartId());
            assertEquals(2, hit.getDistributionKey());
            assertFalse(hit.hasSortData());
            hitNum++;
        }
    }