Vespa#
Vespa serves vector search, BM25 and ranking at scale, self-hosted or on Vespa
Cloud. VespaBackend writes through Vespa’s /document/v1 API and searches through /search/,
with the permission filter in the YQL query, so Vespa applies it inside the nearest-neighbor or
BM25 search: pre-filtering is exact.
pip install "wardowl[vespa]"
The application package needs a schema with the fields the backend writes. schema(dim) writes
one; examples/vespa
is a complete single-node package, which CI deploys.
from pathlib import Path
from wardowl import MetadataACLAuthorizer, PermissionedRetriever
from wardowl.backends.vespa import VespaBackend, schema
Path("app/schemas/wardowl.sd").write_text(schema(768)) # then deploy the package
backend = VespaBackend("http://localhost:8080", embed)
backend.add(chunks) # or backend.add(chunks, embeddings) with vectors you computed
retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())
How filtering works#
Each chunk stores a tagged term for every string or integer in its metadata (the field name, the
value’s type and the value), hex-encoded, in the array attribute wardowl_terms with fast-search.
A filter becomes wardowl_terms in ("...") conditions joined with and and or. The terms keep
1 and "1" apart, as the reference semantics do, and every value the filter writes into the YQL
text is hex digits, so no metadata value can change the query. The metadata itself is stored as
JSON and returned exactly as written. A filter that can match nothing returns no hits without a
query, and filter_to_yql raises NothingMatches for it, since a query without the condition
matches every chunk.
Search modes#
approximate=True(the default) searches the HNSW index with the filter applied during the search; Vespa switches to exact search when the filter admits few chunks.approximate=Falsecompares the query with every chunk the filter admits.mode="keyword"ranks by BM25 on the text, under the same filter. WithRankFusion, a vector backend and a keyword backend on the same schema give hybrid search.
Scores are cosine similarities (cos(distance(field, embedding)) with the angular metric), or BM25
scores in keyword mode.
Schemas, namespaces and tenants#
schema= (wardowl by default) is the schema the backend writes to and searches;
schema(dim, name=...) writes a schema of that name. namespace= (wardowl by default) is only
the namespace part of each document’s path, /document/v1/<namespace>/<schema>/docid/<id>, which
add, set_metadata and delete use. A search selects from the whole schema
(select * from <schema>), whatever its namespace. Two backends with different namespaces on one
schema therefore search each other’s chunks, and both report a chunk by the id it was added with,
so an id written in both is returned twice under one name.
To keep tenants apart in one schema, filter by tenant: put the tenant on every chunk and use
AllOf(TenantAuthorizer(), ...) (Combining). Or give each tenant
its own schema, listed under the content cluster’s <documents> in services.xml (as
<document type="acme" mode="index"/>):
Path("app/schemas/acme.sd").write_text(schema(768, name="acme")) # then deploy the package
acme = VespaBackend("http://localhost:8080", embed, schema="acme")
Production notes#
Vespa Cloud and TLS: pass
client=httpx.Client(cert=..., headers=...)for client certificates, tokens or proxies; the backend uses it as given.Hits per query: Vespa allows 400 hits unless the query profile raises
maxHits, as the example package does. Post-filtering can ask for more thank.Coverage: a search that did not cover every document (a content node down or slow, or a timeout:
coverage.fullfalse, orcoverage.degradedset) raisesRuntimeErrorrather than return its partial results as complete.Writes go
concurrencyrequests at a time (8 by default). A write that fails raises; repeated ids in oneaddkeep the last.Ids: a chunk id goes into the
/document/v1path percent-encoded,/included. The ids.and..are refused withValueErrorbyadd,set_metadataanddelete, before any request: HTTP clients drop such a path segment, which would send the request to another path, and Vespa refuses them percent-encoded too.Visibility: a write is searchable when Vespa acknowledges it, so
addreturning means the chunk is searchable.