This guide explains how to search and rank over structured multivalued fields.
The examples in this guide use the weightedsetfield type.
The generic map<key-type,value-type> field type
does not currently support ranking and can only be used for matching and filtering.
When building a search application we need to think about:
String fields which shares the same match
and linguistic processing settings can be combined using fieldsets.
At query time, we can take the user query and translate it into a valid Vespa query request which implements
our matching and retrieval strategy over the designed document schema.
Ranking documents
The documents which match the query and are retrieved by the query are scored using a ranking model.
Once a document is retrieved by the query logic the document can be scored using the full
flexibility of the Vespa ranking framework.
A minimal Vespa application
In the following sections we explore matching and ranking over multivalued string fields.
Prerequisites:
Linux, macOS or Windows 10 Pro on x86_64 or arm64,
with Podman Desktop or
Docker Desktop installed, with an engine running.
Assuming we have the following sample data document where we have a structured
tag-like field where there is a weight associated with each element.
{"put":"id:photos:photo::0","fields":{"title":"Mira in the sunset","description":"A sunny afternoon with our dogs","tags":{"no filter":1,"light":3,"black and white":3,"clear sky":2,"sunset dogs":4}}}
Structured data like the tags, where we both want to match and rank is best represented using
the weightedsetfield type.
The Vespa weightedset field type can be used to represent:
Editorial ranking overrides, for example sponsored search listings.
How should we design our Vespa schema,
and how should we match and search this data model for end-user free text queries?
We want to use text matching when searching the title and description.
We also want to match the free form tags field as these tags might increase recall
and the weight of the matched element(s) could influence ranking of documents matched - schema:
Assuming a free text query sunset photos featuring dogs,
translate the user query into a Vespa query request using YQL:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=sunset photos featuring dogs' \
'type=all'
The above query returns 0 hits, since the query requires that all query terms matches the document.
By adding trace.level to the query request we can see
how the query is parsed and executed against the content nodes:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=sunset photos featuring dogs' \
'type=all' \
'trace.level=3'
In the trace we can see the query which is dispatched to the content nodes:
query=[AND sunshot photos featuring dogs]
Using tracing is very useful when debugging why documents match or does not match.
Since the sample document does not contain the term featuring or photos,
the query fails to retrieve the example document.
Relax the query matching to instead of requiring that all terms match, to use any.
See model.type query api reference for supported query types:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=sunset photos featuring dogs' \
'type=any'
Changing the type to any, recalls the sample document as we no longer require that all query terms must match.
With type it also possible to require that individual query terms match by using +:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=+sunset photos featuring +dogs' \
'type=any'
In this example sunset and dogs must be matched. Note that we have disabled stemming so querying
for dogs won't recall documents with dog. This is one of the reasons we disabled stemming, to demonstrate
that stemming has impact on recall. Requiring dog will cause the query to not recall our sample document.
$ vespa query 'yql=select * from photos where userQuery()' \
'query=+sunset photos featuring +dog' \
'type=any'
Now, explore how Vespa matches the multivalued tags field of
type weightedset.
Notice that we change back to type=all.
In this example we also use the default-index
query parameter to limit matching to the tags field.
$ vespa query 'yql=select * from photos where userQuery()' \
'query=clear sky' \
'type=all' \
'default-index=tags'
The query matches the document which is no surprise since a tag contains the exact content clear sky.
Let us search for just clear instead:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=clear' \
'type=all' \
'default-index=tags'
Also matches the document, this demonstrates that matching is partial, it does not require
to match the set element exactly. clear matches clear sky and sky will match clear sky.
But what about black sky:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=black sky' \
'type=all' \
'default-index=tags'
Also matches the document. This is an example of cross-element matching. With weightedset
using indexing:index with match:text multi term queries match across elements.
This might be a good decision, as we increase recall,
however in some cases we want to differentiate an exact match from a partial match during ranking,
so that exact matches are ranked higher than partial matches.
Ranking
We have now explored querying and matching, now it's time to focus on how to rank the documents matched.
You might not have noticed, but in the above examples, each of the queries produced a relevance score per hit,
this score was in our previous examples calculated using the default rank profile
which in our case used nativeRank.
We can start by analyzing other rank features by asking Vespa to produce them for us.
We use match-features
to return rank features with the retrieved documents.
We explicitly mention which ranking features we want to have calculated and returned.
Notice that we don't change the actual scoring, we still use nativeRank as the scoring function:
We can also notice that elementCompleteness(tags).fieldCompleteness is 1.0 which means
that the tag was matched exactly and the "elementCompleteness(tags).elementWeight outputs
the weight of the best matched element.
Now we will see a list of features in the response:
$ vespa query 'yql=select * from photos where userQuery()' \
'query=clear sky' 'type=any'
Each hit returned contains a matchfeatures field
where we can see the various scores for the features.
Now, we can include these features in a ranking expression used in first-phase to actually change the ranking.
The actual best scoring function is data dependent.
A trained function using machine learning is by far the easiest way.
The bag of words bm25 ranking feature is not normalized,
so combining it in a linear function is challenging, as the score range of the feature is unbound.
To overcome this, and allow easy exploration without changing the rank profile,
make the parameters in the function overridable on a per-query basis by: