Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

%load_ext autoreload
%autoreload 2
import mycode.vap as vap
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
# The goal is to:
# - Put a document with fields of various types into VESPA
# - Fetch document fields with match features
# - map match features to original document fields
# - render the document using match features as if they were from original document
# Create a sample VAP
from vespa.package import (ApplicationPackage, ServicesConfiguration, Field, Schema, Document, RankProfile, Function)
from vespa.configuration.services import services, container, document_api, search, chain, searcher, \
    document_processing, content, documents, document, redundancy

doc_schema = Schema(
    name="doc",
    document=Document(
        fields=[
            Field(name="id", type="int", indexing=["attribute"]),
            Field(name="byte_field", type="byte", indexing=["attribute"]),
            Field(name="str", type="string", indexing=["attribute"], ),
            Field(name="my_tensor", type="tensor<float>(x[1])", indexing=["attribute"]),
            Field(name="int_array", type="array<int>", indexing=["attribute"]),
            Field(name="str_array", type="array<string>", indexing=["attribute"]),
            Field(name="bool_field", type="bool", indexing=["attribute"]),
            Field(name="double_field", type="double", indexing=["attribute"]),
            Field(name="weightedset_field", type="weightedset<string>", indexing=["attribute"]),
        ]
    ),
    rank_profiles=[
        RankProfile(
            name='fields',
            inherits='unranked',
            first_phase="0",
            functions=[
                Function(name='id', expression='tensorFromLabels(attribute(id))'),
                Function(name='str', expression='tensorFromLabels(attribute(str))'),
                Function(name='my_tensor', expression='attribute(my_tensor)'),
                Function(name='int_array', expression='tensorFromLabels(attribute(int_array))'),
                Function(name='str_array', expression='tensorFromLabels(attribute(str_array))'),
                Function(name='bool_field', expression='attribute(bool_field)'),
                Function(name='double_field', expression='attribute(double_field)'),
                Function(name='byte_field', expression='tensorFromLabels(attribute(byte_field))'),
                Function(name='weightedset_field', expression='tensorFromWeightedSet(attribute(weightedset_field))'),
            ],
            match_features=[
                'id', 'str', 'int_array', 'str_array', 'my_tensor', 'bool_field', 'double_field', 'byte_field',
                'weightedset_field'
            ],
        )
    ]
)

services_config = ServicesConfiguration(
    application_name="test",
    services_config=services(
        container(
            document_api(),
            document_processing(),
            search(
                chain(
                    searcher(
                        id="lt.jocas.examples.MatchFeaturesMapperSearcher",
                        bundle="match-features-mapper-bundle",
                    ),
                    id="match_features_mapper",
                    inherits='vespa'
                )
            ),
            id="container",
            version="1.0"
        ),
        content(
            redundancy(1),
            documents(
                document(type="doc", mode="index"),
            ),
            id="content",
            version="1.0"
        )
    )
)

application_package = ApplicationPackage(
    name='test',
    schema=[doc_schema],
    services_config=services_config,
)
# Package into a bundle jar
!(cd ../examples/match-features-mapper-bundle && mvn -f pom.xml package)
# There should be a file `../examples/match-features-mapper-bundle/target/match-features-mapper-bundle-0.0.1-deploy.jar`
!ls -alh ../examples/match-features-mapper-bundle/target/match-features-mapper-bundle-0.0.1-deploy.jar
# The relevant code is
!cat ../examples/match-features-mapper-bundle/src/main/java/lt/jocas/examples/MatchFeaturesMapperSearcher.java
package lt.jocas.examples;

import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.TextNode;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.result.FeatureData;
import com.yahoo.search.result.Hit;
import com.yahoo.search.schema.Field;
import com.yahoo.search.schema.Schema;
import com.yahoo.search.schema.SchemaInfo;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.tensor.Tensor;
import com.yahoo.tensor.TensorAddress;

import javax.inject.Inject;
import java.util.List;
import java.util.Set;

public class MatchFeaturesMapperSearcher extends Searcher {

    private final static String MF = "matchfeatures";
    // From here https://docs.vespa.ai/en/exposing-schema-information.html
    private final SchemaInfo schemaInfo;
    private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance;

    @Inject
    public MatchFeaturesMapperSearcher(SchemaInfo schemaInfo) {
        this.schemaInfo = schemaInfo;
    }

    @Override
    public Result search(Query query, Execution execution) {
        return execution.search(query);
    }

    private boolean isMatchFeaturesInSummary(Result result) {
        return result.getQuery().getPresentation().getSummaryFields().contains(MF);
    }

    private Object arrayFromTensorLabels(Tensor tensor) {
        Set<TensorAddress> tensorAddresses = tensor.cells().keySet();
        List<TextNode> labels = tensorAddresses.stream()
                .map(tensorAddress -> tensorAddress.label(0))
                .map(jsonNodeFactory::textNode)
                .toList();
        return jsonNodeFactory.arrayNode().addAll(labels);
    }

    private Object fromTensor(Tensor tensor, Field.Type.Kind kind) {
        Set<TensorAddress> tensorAddresses = tensor.cells().keySet();

        if (tensor.type().rank() == 0)
            return tensor.asDouble();

        if (tensorAddresses.size() == 1) {
            return tensorAddresses.iterator().next().label(0);
        }
        return arrayFromTensorLabels(tensor);
    }

    private boolean boolFromTensor(Tensor tensor) {
        Double value = tensor.cells().values().iterator().next();
        return !value.equals(0.0d);
    }

    private long longFromTensor(Tensor tensor) {
        Set<TensorAddress> tensorAddresses = tensor.cells().keySet();
        String label = tensorAddresses.iterator().next().label(0);
        return Long.parseLong(label);
    }

    private void handleHit(Hit hit, Schema schema) {
        var matchFeatures = (FeatureData) hit.fields().get(MF);
        for (String featureName : matchFeatures.featureNames()) {
            Tensor tensor = matchFeatures.getTensor(featureName);

            if (!schema.fields().containsKey(featureName)) {
                // Schema does not have a named field then just remap
                hit.setField(featureName, tensor);
            } else {
                // Schema contains a named field then make values correctly renderable
                Field.Type.Kind kind = schema.fields().get(featureName).type().kind();
                var value = switch (kind) {
//                        case ANNOTATIONREFERENCE -> null;
                    case ARRAY -> arrayFromTensorLabels(tensor);
                    case BOOL -> boolFromTensor(tensor);
                    case BYTE, INT, LONG -> longFromTensor(tensor);
                    case FLOAT, DOUBLE -> tensor.asDouble(); // no need to wrap it
//                        case MAP -> null;
//                        case POSITION -> null;
//                        case PREDICATE -> null;
//                        case RAW -> null;
//                        case REFERENCE -> null;
                    case STRING -> fromTensor(tensor, kind);
//                        case STRUCT -> null;
                    case TENSOR -> tensor;
//                        case URL -> null;
//                        case WEIGHTEDSET -> null;
                    default -> tensor;
                };
                hit.setField(featureName, value);
            }
        }
        hit.removeField(MF);
    }

    private void fromMatchFeatures(Result result) {
        // If matchfeatures are not asked, then we have nothing to do
        if (!isMatchFeaturesInSummary(result)) return;

        Set<String> restrict = result.getQuery().getModel().getRestrict();
        // If more than one schema is searched, then we have to have the sddocname
        // in each hit to properly resolve, which requires summary filling...
        if (restrict.size() != 1) return;

        String schemaName = restrict.iterator().next();
        Schema schema = schemaInfo.schemas().get(schemaName);

        result.hits().forEach(hit -> handleHit(hit, schema));
    }

    @Override
    public void fill(Result result, String summaryClass, Execution execution) {
        execution.fill(result, summaryClass);
        fromMatchFeatures(result);
    }
}
from pathlib import Path

# Currently, pyvespa provides no way to add bundle jars nicely into VAP
# So, we need to do a little dance and add it manually.

tmp_vap_folder = '_tmp'
application_package.to_files(Path(tmp_vap_folder))

vap.add_bundles(
    application_root=tmp_vap_folder,
    bundles=['../examples/match-features-mapper-bundle/target/match-features-mapper-bundle-0.0.1-deploy.jar']
)

from vespa.deployment import VespaDocker

# In case running colima on macos run the following
# !sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
vespa_docker = VespaDocker(
    container_image="vespaengine/vespa:8.588.8",
)

client = vespa_docker.deploy_from_disk(
    application_name='test',
    application_root=Path(tmp_vap_folder),
)
def compile_deploy():
    a = !(cd ../examples/match-features-mapper-bundle && mvn -f pom.xml package)
    tmp_vap_folder = '_tmp'
    application_package.to_files(Path(tmp_vap_folder))
    vap.add_bundles(
        application_root=tmp_vap_folder,
        bundles=['../examples/match-features-mapper-bundle/target/match-features-mapper-bundle-0.0.1-deploy.jar']
    )
    vap.redeploy_from_disk(
        docker=vespa_docker,
        application_root=tmp_vap_folder
    )
compile_deploy()
Deploy status code: 200
# Create and feed 1 dummy doc
docs = [
    {
        'id': f'{1}',
        'fields': {
            'id': 1,
            'str': 'foo',
            'int_array': [1, 2],
            'str_array': ['one', 'two'],
            'my_tensor': [2],
            'bool_field': True,
            'double_field': 1.23,
            'byte_field': 12,
            'weightedset_field': {'bar': 9, 'baz': 8},
        }
    }
]

client.feed_iterable(docs, schema="doc", namespace="doc", callback=vap.feed_callback)
compile_deploy()
client.query(body={
    'yql': 'select documentid, matchfeatures from sources doc where true',
    'searchChain': 'match_features_mapper',
    'model.restrict': 'doc',
    'ranking': 'fields',
    # 'ranking': 'unranked',
    'presentation.summary': 'default',
    # 'trace.level': 1,
    'presentation.format.tensors': 'short-value',
}).json
{'root': {'id': 'toplevel', 'relevance': 1.0, 'fields': {'totalCount': 1}, 'coverage': {'coverage': 100, 'documents': 1, 'full': True, 'nodes': 1, 'results': 1, 'resultsFull': 1}, 'children': [{'id': 'id:doc:doc::1', 'relevance': 0.0, 'source': 'content', 'fields': {'bool_field': True, 'byte_field': 12, 'double_field': 1.23, 'id': 1, 'int_array': ['1', '2'], 'my_tensor': [2.0], 'str': 'foo', 'str_array': ['one', 'two'], 'weightedset_field': {'bar': 9.0, 'baz': 8.0}, 'documentid': 'id:doc:doc::1'}}]}}