Home / AI & GenAI / Vector Databases

AI & GenAIHow-toIntermediate

Tuning Weaviate hybrid search: alpha, fusion type, BM25 settings and tokenization for better RAG retrieval

Weaviate's hybrid query blends BM25 keyword search and vector search, but the defaults rarely fit every corpus. Build an evaluation set, then tune alpha, the fusion method, which properties BM25 searches, tokenization and the BM25 parameters - one change at a time.

Deepak Bhardwaj4 min read

Applies toWeaviate 1.24+ (relativeScoreFusion default from 1.24), Python client v4; Weaviate Cloud and self-hosted

The hybrid query

from weaviate.classes.query import HybridFusion, MetadataQuery

kb = client.collections.get("KbChunk")
res = kb.query.hybrid(
    query="ORA-01555 snapshot too old during nightly export",
    alpha=0.5,
    fusion_type=HybridFusion.RELATIVE_SCORE,
    limit=10,
    return_metadata=MetadataQuery(score=True, explain_score=True),
)
for o in res.objects:
    print(round(o.metadata.score, 3), o.properties["doc_id"])
Parameter Meaning
alpha 0 = pure BM25, 1 = pure vector, 0.5 = equal
fusion_type RELATIVE_SCORE (normalises scores, default in recent versions) or RANKED (reciprocal-rank style)
query_properties Which text properties BM25 searches, with optional boosts
vector Provide your own query vector instead of the vectorizer
max_vector_distance Drop vector results beyond a distance (newer versions)

Step 1: build an evaluation set first

Without measurement, tuning is guesswork. Collect 100–300 real questions with the document(s) that answer them:

eval_set = [
  {"q": "ORA-01555 during export", "expected": {"kb-ora-01555"}},
  {"q": "users can't connect to VPN after password change", "expected": {"kb-vpn-cred-cache"}},
  # ...
]

def hit_rate(k=5, **params):
    hits = 0
    for e in eval_set:
        r = kb.query.hybrid(query=e["q"], limit=k, **params)
        got = {o.properties["doc_id"] for o in r.objects}
        hits += bool(got & e["expected"])
    return hits / len(eval_set)

Tag each question as keyword-heavy (codes, names, versions) or semantic (paraphrased problem descriptions), and report both.

Step 2: sweep alpha

for a in [0.0, 0.25, 0.4, 0.5, 0.6, 0.75, 1.0]:
    print(a, hit_rate(alpha=a))

Typical pattern:

Corpus Best alpha tends to be
Technical docs with many error codes and product names 0.3–0.5
Natural-language FAQs and policies 0.6–0.8
Short titles or product catalogs Lower (more keyword)

Pick the value that does well on both slices rather than the best average.

Step 3: compare fusion types

for f in [HybridFusion.RELATIVE_SCORE, HybridFusion.RANKED]:
    print(f, hit_rate(alpha=0.5, fusion_type=f))

RANKED only uses positions; RELATIVE_SCORE keeps information about how much better the top result is. Relative score usually wins when one side has a clear strong match; test on your data.

Step 4: control which properties BM25 searches

By default BM25 searches all text properties. Long noisy fields (raw HTML, logs) dilute it. Choose and boost:

res = kb.query.hybrid(
    query=q, alpha=0.4,
    query_properties=["title^3", "content"],   # title matches count 3x
)

Make sure properties that should never be keyword-searched (URLs, JSON blobs) are created with index_searchable=False.

Step 5: tokenization

Tokenization is set per property at creation and decides how BM25 splits text:

Tokenization Splits Good for
word (default) On non-alphanumeric chars, lowercased Normal prose
lowercase / whitespace On whitespace only Keeping ORA-01555, KB5034441, v1.2.3 as single tokens
field Whole value is one token IDs, exact categories
trigram 3-character grams Typos, partial matches
gse, kagome_ja, kagome_kr Language-specific Chinese, Japanese, Korean

With word, ORA-01555 becomes ora and 01555, which still often works but also matches every other ORA- code. For codes, add a separate property with whitespace or field tokenization and include it in query_properties with a boost:

from weaviate.classes.config import Property, DataType, Tokenization
Property(name="error_codes", data_type=DataType.TEXT_ARRAY, tokenization=Tokenization.FIELD)

Tokenization can't be changed on an existing property; add a new property and re-import that field.

Step 6: BM25 parameters

Set per collection (inverted_index_config):

from weaviate.classes.config import Configure
Configure.inverted_index(bm25_b=0.75, bm25_k1=1.2)   # defaults
Parameter Effect
k1 Term frequency saturation; higher = repeated terms count more
b Length normalisation; lower = long chunks penalised less

Change these only after alpha, properties and tokenization; effects are usually smaller. Stopword settings are also in the inverted index config.

Step 7: add a reranker

Hybrid gets the right chunk into the top 20; a reranker module (for example reranker-cohere or a reranker-transformers container) can reorder the top results:

from weaviate.classes.query import Rerank
res = kb.query.hybrid(query=q, alpha=0.4, limit=20,
                      rerank=Rerank(prop="content", query=q))

Measure hit rate@3 with and without, and the latency it adds.

Tuning order

Order Change Needs re-import?
1 alpha No
2 fusion_type No
3 query_properties and boosts No
4 Tokenization / extra code fields Yes
5 BM25 k1, b No (collection config update)
6 Reranker No (module required)

Prevent regressions next time

  • Keep the evaluation set in the repo and run it in CI when chunking, model or query settings change.
  • Log the alpha and fusion type used with each production query for later analysis.
  • Add identifier fields with exact tokenization at design time.
  • Re-tune after major corpus changes, not just once.