Files
lancedb/python/python/lancedb/rerankers/voyageai.py
T
Yash Raj PandeyandClaude Opus 5.5 5fa30ae404 fix(python): honor explicit Cohere and VoyageAI reranker API keys (#4546)
`CohereReranker` and `VoyageAIReranker` read the environment variable
before the `api_key` argument. When `COHERE_API_KEY` or `VOYAGE_API_KEY`
is set, the reranker ignores an explicit `api_key` and uses the account
from the environment.

The docs say the opposite. `docs/web/integrations/reranking/cohere.mdx`
and `voyageai.mdx` use the environment variable only when `api_key` is
not provided. `JinaReranker` and `WatsonxReranker` already put the
explicit key first, and #4515 makes the same change for
`OpenaiReranker`.

## Fix

`CohereReranker._client` and `VoyageAIReranker._client` now use
`self.api_key` first and the environment variable as the fallback.

## Tests

`python/python/tests/test_reranker_credentials.py` replaces the SDK
module with a stub whose `Client` records the API key. It covers both
keys set, each key alone, and no key. With the stub, the test runs
without `cohere` or `voyageai` installed.

- On `main`, the 2 cases with both keys set fail and the other 6 pass.
With the fix, all 8 pass.
- `test_rerankers.py` with the new file: 24 passed, 13 skipped (the
skipped tests need real API keys).
- `ruff format --check` and `ruff check` (0.9.9) pass on the changed
files.
- I did not build the native extension. I ran the tests against the
lancedb 0.40.0 wheel with the changed files copied over it. In that
wheel, both reranker files are byte-identical to `main` before this
change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:22:53 +08:00

123 lines
4.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import os
from functools import cached_property
from typing import Optional
import pyarrow as pa
from ..util import attempt_import_or_raise
from .base import Reranker
class VoyageAIReranker(Reranker):
"""
Reranks the results using the VoyageAI Rerank API.
https://docs.voyageai.com/docs/reranker
Parameters
----------
model_name : str, default "rerank-english-v2.0"
The name of the cross encoder model to use. Available voyageai models are:
- rerank-2.5
- rerank-2.5-lite
- rerank-2
- rerank-2-lite
column : str, default "text"
The name of the column to use as input to the cross encoder model.
top_n : int, default None
The number of results to return. If None, will return all results.
return_score : str, default "relevance"
options are "relevance" or "all". Only "relevance" is supported for now.
api_key : str, default None
The API key to use. If None, will use the VOYAGE_API_KEY environment variable.
truncation : Optional[bool], default None
"""
def __init__(
self,
model_name: str,
column: str = "text",
top_n: Optional[int] = None,
return_score="relevance",
api_key: Optional[str] = None,
truncation: Optional[bool] = True,
):
super().__init__(return_score)
self.model_name = model_name
self.column = column
self.top_n = top_n
self.api_key = api_key
self.truncation = truncation
def __str__(self):
return f"VoyageAIReranker(model_name={self.model_name})"
@cached_property
def _client(self):
voyageai = attempt_import_or_raise("voyageai")
if os.environ.get("VOYAGE_API_KEY") is None and self.api_key is None:
raise ValueError(
"VOYAGE_API_KEY not set. Either set it in your environment or \
pass it as `api_key` argument to the VoyageAIReranker."
)
return voyageai.Client(
api_key=self.api_key or os.environ.get("VOYAGE_API_KEY"),
)
def _rerank(self, result_set: pa.Table, query: str):
result_set = self._handle_empty_results(result_set)
if len(result_set) == 0:
return result_set
docs = result_set[self.column].to_pylist()
response = self._client.rerank(
query=query,
documents=docs,
top_k=self.top_n,
model=self.model_name,
truncation=self.truncation,
)
results = (
response.results
) # returns list (text, idx, relevance) attributes sorted descending by score
indices, scores = list(
zip(*[(result.index, result.relevance_score) for result in results])
) # tuples
result_set = result_set.take(list(indices))
# add the scores
result_set = result_set.append_column(
"_relevance_score", pa.array(scores, type=pa.float32())
)
return result_set
def rerank_hybrid(
self,
query: str,
vector_results: pa.Table,
fts_results: pa.Table,
):
if self.score == "all":
combined_results = self._merge_and_keep_scores(vector_results, fts_results)
else:
combined_results = self.merge_results(vector_results, fts_results)
combined_results = self._rerank(combined_results, query)
if self.score == "relevance":
combined_results = self._keep_relevance_score(combined_results)
return combined_results
def rerank_vector(self, query: str, vector_results: pa.Table):
vector_results = self._rerank(vector_results, query)
if self.score == "relevance":
vector_results = vector_results.drop_columns(["_distance"])
return vector_results
def rerank_fts(self, query: str, fts_results: pa.Table):
fts_results = self._rerank(fts_results, query)
if self.score == "relevance":
fts_results = fts_results.drop_columns(["_score"])
return fts_results