Ranking Strategy Testing Guide¶
This guide covers testing the production Vespa search path (VespaSearchBackend.search(query_dict)) with all available ranking strategies. Coverage lives in tests/runtime/integration/test_ranking_strategies_real.py — a parametrized integration test that drives every ranking strategy (each a rank-profile-name string) against a real Vespa container with seeded ColPali embeddings.
Run the test¶
What it covers¶
The test fixture chain builds the full real backend on demand:
- Real Vespa:
vespa_instancefixture is a compatibility shim backed by the session-scopedshared_vespacontainer (seetests/conftest.py), withvideo_colpali_smol500_mv_frame_test_unitdeployed at test setup. - Real vLLM ColPali:
remote_inferenceresolves the cogniverse-e2e cluster'svllm_colpaliservice servingTomoroAI/tomoro-colqwen3-embed-4b(pooling runner,embedconvert mode, 320-dim per-token embeddings) and bindsRemoteColPaliLoaderagainst it. - Real seed corpus: three documents with real per-token ColPali embeddings (sunset / ocean / forest scenes with matching transcripts) fed into Vespa via
seeded_ranking_corpus.
There is also TestAutoSelectDefaultRanking, which omits strategy entirely and asserts the backend falls back to the schema's default rank profile (the contract SearchAgent relies on after dropping its hardcoded binary_binary default).
Each ranking strategy is a plain rank-profile-name string passed as the strategy key of the query_dict. Every strategy is exercised end-to-end through VespaSearchBackend.search:
| Class | Strategies | Inputs |
|---|---|---|
| Text-only | bm25_only, bm25_no_description | Text query |
| Visual | float_float, binary_binary, float_binary, phased | query_embeddings via RemoteColPaliLoader(...).load_model() client's process_queries |
| Hybrid | hybrid_float_bm25, hybrid_binary_bm25, hybrid_bm25_binary, hybrid_bm25_float, plus _no_description variants | Text + query_embeddings |
Each case asserts on the returned List[SearchResult]: - Non-empty results from the seeded corpus. - Descending relevance order by result.score. - result.document.metadata["source_id"] falls within the seeded corpus.
Prerequisites¶
dockeravailable (Vespa runs in a container).- The cogniverse-e2e cluster serving
vllm_colpali(kubectl --context k3d-cogniverse-e2e -n cogniverse get deploy cogniverse-vllm-colpali); tests never start a model on this host. - Environment variables
BACKEND_URL/BACKEND_PORTset (or aconfigs/config.jsonwithbackend.url/backend.port) if constructing aConfigManagerviacreate_default_config_manager()outside the test fixtures, e.g. for the manual REPL usage below.
Strategy reference¶
| Strategy | Type | Speed | Accuracy | Requirements |
|---|---|---|---|---|
bm25_only | Text | Fast | Good | Text query |
bm25_no_description | Text | Fast | Good | Text query |
float_float | Visual | Slow | Highest | Embeddings |
binary_binary | Visual | Fastest | Good | Embeddings |
float_binary | Visual | Fast | Very Good | Embeddings |
phased | Visual | Fast | High | Embeddings |
hybrid_float_bm25 | Hybrid | Slow | Highest | Text + embeddings |
hybrid_binary_bm25 | Hybrid | Fast | Good | Text + embeddings |
hybrid_bm25_binary | Hybrid | Fast | Good | Text + embeddings |
hybrid_bm25_float | Hybrid | Medium | Very Good | Text + embeddings |
hybrid_float_bm25_no_description | Hybrid | Slow | High | Text + embeddings |
hybrid_binary_bm25_no_description | Hybrid | Fast | Good | Text + embeddings |
hybrid_bm25_binary_no_description | Hybrid | Fast | Good | Text + embeddings |
hybrid_bm25_float_no_description | Hybrid | Medium | Very Good | Text + embeddings |
The schema also defines a default rank profile (not in the table above, since no test parametrizes it by name) that TestAutoSelectDefaultRanking exercises indirectly by omitting strategy from the query dict.
Manual usage from a Python REPL¶
from cogniverse_vespa.search_backend import VespaSearchBackend
from cogniverse_foundation.config.utils import create_default_config_manager
from cogniverse_core.schemas.filesystem_loader import FilesystemSchemaLoader
from pathlib import Path
config_manager = create_default_config_manager()
schema_loader = FilesystemSchemaLoader(Path("configs/schemas"))
backend = VespaSearchBackend(
config={
"url": "http://localhost",
"port": 8080,
"profiles": {
"test_colpali": {
"type": "video",
"schema_name": "video_colpali_smol500_mv_frame",
}
},
# default_profiles[type] is a dict with "profile" (and optional
# "strategy") keys — only consulted when >1 profile exists for a
# type and query_dict omits "profile"/"strategy".
"default_profiles": {"video": {"profile": "test_colpali"}},
},
config_manager=config_manager,
schema_loader=schema_loader,
)
# Text-only — strategy is a rank-profile-name string. "schema_name" above
# is the base name; search() appends the tenant suffix internally, so
# tenant_id="test:unit" resolves to "video_colpali_smol500_mv_frame_test_unit".
results = backend.search({
"query": "buck",
"type": "video",
"profile": "test_colpali",
"strategy": "bm25_only",
"top_k": 3,
"tenant_id": "test:unit",
})
for result in results:
print(result.score, result.document.metadata["source_id"])
# Visual / hybrid — pass pre-computed embeddings via `query_embeddings`
# (see test_ranking_strategies_real.py for how to encode a query through
# the cluster's vLLM service: RemoteColPaliLoader(...).load_model() returns a client
# whose .process_queries is bound to RemoteInferenceClient.process_queries_vllm).
Query tensor format¶
VespaSearchBackend formats query embeddings for Vespa via _format_query_vector_param (defined in cogniverse_vespa/search_backend.py):
- Single-vector schemas (LVT sv_chunk): numpy array flattened to a dense list —
tensor(v[dim])binding (e.g.tensor<float>(v[768])). A(1, dim)array is flattened before serialisation. - Multi-vector schemas (ColPali, X-CLIP mv_chunk): numpy array converted to a
{str(token_index): vector_list}dict —tensor<float>(querytoken{}, v[dim])ortensor<int8>(querytoken{}, v[dim])binding (the query-side input tensor; the stored document embedding field usestensor<bfloat16>(patch{}, v[dim]), which is a different tensor from what queries bind to).
Schema arity is determined by _is_single_vector_schema(schema_name) from cogniverse_vespa.embedding_processor. Pass the raw numpy array from your encoder; the formatting is handled internally.
The bound input name depends on the rank profile's declared inputs (see configs/schemas/ranking_strategies.json):
| Input name | Type | Used by |
|---|---|---|
qt | float | ColPali/X-CLIP float_float, float_binary, phased, hybrid_*float* strategies |
qtb | int8 (binary) | ColPali/X-CLIP binary_binary, float_binary, phased, hybrid_*binary* strategies |
acoustic_query | float | audio schema strategies only (e.g. audio_content); same code branch as qt |
q | generic | schemas whose rank profiles declare a bare q input (e.g. wiki_pages, agent_memories) |
Unrecognised input names are logged and skipped rather than raising, so a schema/strategy mismatch surfaces as missing ranking signal, not an error.
Related VespaSearchBackend coverage¶
This guide's real-Vespa/real-vLLM sweep in test_ranking_strategies_real.py covers rank-profile correctness only. Other aspects of VespaSearchBackend are covered by separate test files:
| File | Covers |
|---|---|
tests/backends/unit/test_build_query_inputs.py | _build_query binds every declared float input (qt/qtb/q/acoustic_query), not just the common ones |
tests/backends/unit/test_filter_condition_quoting.py | _build_filter_conditions produces well-formed, correctly quoted YQL for every filter shape |
tests/backends/unit/test_search_backend_dynamic_profiles.py | Per-request profile resolution: a profile the searching tenant stored after the backend was built resolves, another tenant's is neither resolved nor listed, a deleted one stops resolving, concurrent tenants each resolve their own, and an unreadable store raises |
tests/backends/unit/test_search_metrics.py | SearchMetrics latency window stays bounded |
tests/backends/integration/test_dynamic_profile_search_visibility.py | A profile added at runtime is immediately searchable by its tenant alone, and stops resolving once deleted (real Vespa) |
tests/runtime/integration/test_dynamic_profile_visibility.py | POST /admin/profiles → backend.search() sees the new profile end-to-end for its tenant, not for another |
tests/runtime/integration/test_runtime_profile_visibility.py | Across worker processes: a profile added or deleted on one worker reaches another worker's searches within the config manager's staleness bound; another tenant never resolves it; a document fed into a runtime-added profile is stored embedded and searchable |
tests/runtime/integration/test_search_integration.py | Full router → ConfigManager → BackendRegistry → SchemaLoader wiring with a real ColPali query encoder |
tests/runtime/integration/test_export_embeddings_real_vespa.py | export_embeddings filtering and Document-v1 selection escaping |
tests/runtime/integration/test_tenant_extensibility.py | Tenant-scoped instructions/memory round-tripped through the real Vespa config store |