Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c722635af | |||
| 8cc86f9d64 | |||
| bcc8d1c96e | |||
| acdd04eed1 | |||
| 534d2e2454 |
+1
-1
@@ -31,6 +31,7 @@ and this project adheres to
|
||||
- ✨(backend) add deletion endpoint
|
||||
- ✨(backend) add path filter
|
||||
- ✨(backend) add search_type param
|
||||
- ✨(backend) implement reranker #47
|
||||
|
||||
## Changed
|
||||
|
||||
@@ -42,4 +43,3 @@ and this project adheres to
|
||||
|
||||
- 🐛(backend) fix missing index creation in 'index/' view
|
||||
- 🐛(backend) fix parallel test execution issues
|
||||
- 🐛(backend) fix search type value #68
|
||||
|
||||
@@ -20,7 +20,7 @@ class SearchTypeEnum(str, Enum):
|
||||
"""Search type options"""
|
||||
|
||||
HYBRID = "hybrid"
|
||||
FULL_TEXT = "full-text"
|
||||
FULL_TEXT = "full_text"
|
||||
|
||||
|
||||
# Fields
|
||||
|
||||
@@ -18,7 +18,7 @@ from pydantic import (
|
||||
)
|
||||
|
||||
from . import enums
|
||||
from .services.opensearch import check_hybrid_search_enabled
|
||||
from .enums import SearchTypeEnum
|
||||
|
||||
|
||||
class DocumentSchema(BaseModel):
|
||||
@@ -119,22 +119,8 @@ class SearchQueryParametersSchema(BaseModel):
|
||||
order_by: Optional[Literal[enums.ORDER_BY_OPTIONS]] = Field(default=enums.RELEVANCE)
|
||||
order_direction: Optional[Literal["asc", "desc"]] = Field(default="desc")
|
||||
nb_results: Optional[conint(ge=1, le=300)] = Field(default=50)
|
||||
search_type: Optional[enums.SearchTypeEnum] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_default_search_type(self):
|
||||
"""
|
||||
Set default search_type dynamically.
|
||||
If search_type is not provided, it will be set to hybrid if it is configured
|
||||
and fall back on full text otherwise.
|
||||
"""
|
||||
if self.search_type is None:
|
||||
self.search_type = (
|
||||
enums.SearchTypeEnum.HYBRID
|
||||
if check_hybrid_search_enabled()
|
||||
else enums.SearchTypeEnum.FULL_TEXT
|
||||
)
|
||||
return self
|
||||
search_type: Optional[SearchTypeEnum] = Field(default=None)
|
||||
rerank: Optional[bool] = Field(default=None)
|
||||
|
||||
|
||||
class DeleteDocumentsSchema(BaseModel):
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Reranking utilities using rerankers library."""
|
||||
|
||||
import logging
|
||||
from functools import cache
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from rerankers import Reranker # pylint: disable=import-error
|
||||
from rerankers.models.ranker import BaseRanker # pylint: disable=import-error
|
||||
|
||||
from core.services.indexing import format_document
|
||||
from core.utils import get_language_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def rerank(query: str, hits: list[dict]) -> list[dict]:
|
||||
"""
|
||||
Rerank search results using the configured reranker model.
|
||||
|
||||
Args:
|
||||
query: The search query string
|
||||
hits: List of OpenSearch hit objects with _source containing title and content
|
||||
|
||||
Returns:
|
||||
List of reranked results in the same format as input with a _reranked_score
|
||||
"""
|
||||
|
||||
reranker = get_reranker()
|
||||
if reranker is None:
|
||||
logger.warning("Could not import reranker, returning original results")
|
||||
return hits
|
||||
|
||||
try:
|
||||
return _rerank(reranker, query, hits)
|
||||
|
||||
except Exception as e: # noqa: BLE001# pylint: disable=broad-exception-caught
|
||||
logger.error("Reranking failed: %s, returning original results", str(e))
|
||||
return hits
|
||||
|
||||
|
||||
@cache
|
||||
def get_reranker() -> BaseRanker | None:
|
||||
"""
|
||||
Get the reranker instance.
|
||||
Returns None if the reranker library is not available or if initialization fails but
|
||||
does not raise an exception to avoid crashing the application.
|
||||
"""
|
||||
try:
|
||||
logger.info("Initializing reranker model: %s", settings.RERANKER_MODEL_NAME)
|
||||
return Reranker(
|
||||
settings.RERANKER_MODEL_NAME,
|
||||
model_type=settings.RERANKER_MODEL_TYPE,
|
||||
api_key=settings.RERANKER_API_KEY,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001# pylint: disable=broad-exception-caught
|
||||
logger.error("Failed to initialize reranker: %s", str(e))
|
||||
return None
|
||||
|
||||
|
||||
def _rerank(reranker: BaseRanker, query: str, original_hits: list[dict]) -> list[dict]:
|
||||
"""Rerank the original results using the provided reranker."""
|
||||
docs, doc_ids = prepare_rerank_data(original_hits)
|
||||
|
||||
logger.info("Reranking %d results for query: %s", len(original_hits), query)
|
||||
reranked = reranker.rank(query=query, docs=docs, doc_ids=doc_ids)
|
||||
|
||||
reranked_results: list[dict] = []
|
||||
for reranked_result in reranked.results:
|
||||
matching_hits = [
|
||||
hit for hit in original_hits if hit["_id"] == reranked_result.doc_id
|
||||
]
|
||||
|
||||
if not matching_hits:
|
||||
logger.warning(
|
||||
"Reranked document ID %s not found in original hits, skipping",
|
||||
reranked_result.doc_id,
|
||||
)
|
||||
continue
|
||||
|
||||
if len(matching_hits) > 1:
|
||||
logger.warning(
|
||||
"Multiple hits found for document ID %s, using first match",
|
||||
reranked_result.doc_id,
|
||||
)
|
||||
|
||||
hit = matching_hits[0]
|
||||
hit["_reranked_score"] = reranked_result.score
|
||||
reranked_results.append(hit)
|
||||
|
||||
logger.info("Reranking completed, returned %d results", len(reranked_results))
|
||||
|
||||
return reranked_results
|
||||
|
||||
|
||||
def prepare_rerank_data(original_hits: list[dict]) -> tuple[list[str], list[str]]:
|
||||
"""
|
||||
Prepare the documents for reranking by extracting the title and content from the original hits.
|
||||
"""
|
||||
docs = []
|
||||
doc_ids = []
|
||||
for hit in original_hits:
|
||||
title = get_language_value(hit["_source"], "title")
|
||||
content = get_language_value(hit["_source"], "content")
|
||||
|
||||
docs.append(format_document(title, content))
|
||||
doc_ids.append(hit["_id"])
|
||||
|
||||
return docs, doc_ids
|
||||
|
||||
|
||||
def should_rerank(is_rerank_requested: bool | None) -> bool:
|
||||
"""
|
||||
Determine whether to perform reranking based on the input parameter and settings.
|
||||
logs warning if reranking was explicitly requested but the reranker is disabled in settings.
|
||||
falls back to settings.RERANKER_ENABLED if is_rerank_requested is None.
|
||||
"""
|
||||
if is_rerank_requested and not settings.RERANKER_ENABLED:
|
||||
logger.warning(
|
||||
"Reranking was explicitly requested but the reranker "
|
||||
"is disabled in settings. Reranking skipped."
|
||||
)
|
||||
return False
|
||||
|
||||
if is_rerank_requested is None:
|
||||
return settings.RERANKER_ENABLED
|
||||
|
||||
return is_rerank_requested and settings.RERANKER_ENABLED
|
||||
@@ -9,6 +9,7 @@ from core.enums import SearchTypeEnum
|
||||
|
||||
from .embedding import embed_text
|
||||
from .opensearch import check_hybrid_search_enabled, opensearch_client
|
||||
from .reranking import rerank, should_rerank
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +28,7 @@ def search( # noqa : PLR0913
|
||||
tags,
|
||||
search_type,
|
||||
path=None,
|
||||
rerank_requested=None,
|
||||
):
|
||||
"""Perform an OpenSearch search"""
|
||||
query = get_query(
|
||||
@@ -40,7 +42,7 @@ def search( # noqa : PLR0913
|
||||
path=path,
|
||||
search_type=search_type,
|
||||
)
|
||||
return opensearch_client().search( # pylint: disable=unexpected-keyword-arg
|
||||
response = opensearch_client().search( # pylint: disable=unexpected-keyword-arg
|
||||
index=",".join(search_indices),
|
||||
body={
|
||||
"_source": enums.SOURCE_FIELDS, # limit the fields to return
|
||||
@@ -62,6 +64,14 @@ def search( # noqa : PLR0913
|
||||
ignore_unavailable=True,
|
||||
)
|
||||
|
||||
if should_rerank(rerank_requested) and q != "*":
|
||||
response["hits"]["hits"] = rerank(
|
||||
query=q,
|
||||
hits=response["hits"]["hits"],
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||
def get_query( # noqa : PLR0913
|
||||
|
||||
@@ -8,6 +8,7 @@ of documents is slow and better done only once.
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
@@ -94,7 +95,7 @@ def test_api_documents_search_opensearch_env_variables_not_set(settings):
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_search_query_unknown_user(settings):
|
||||
"""Searching a document without an existing user should result in a 400 error"""
|
||||
"""Searching a document without an existing user should result in a 401 error"""
|
||||
setup_oicd_resource_server(
|
||||
responses,
|
||||
settings,
|
||||
@@ -993,3 +994,36 @@ def test_api_documents_search_filtering_by_path(settings):
|
||||
assert len(response.json()) == 2
|
||||
for hit in response.json():
|
||||
assert hit["_source"]["path"].startswith(path_filter)
|
||||
|
||||
|
||||
@responses.activate
|
||||
@mock.patch("core.services.reranking.rerank")
|
||||
def test_api_documents_rerank_blocked(
|
||||
mock_rerank,
|
||||
settings,
|
||||
caplog,
|
||||
):
|
||||
"""Test can disable rerank at run time even when enabled in settings"""
|
||||
settings.RERANKER_ENABLED = True
|
||||
|
||||
setup_oicd_resource_server(responses, settings, sub="user_sub")
|
||||
service = factories.ServiceFactory()
|
||||
|
||||
documents = bulk_create_documents([{"title": "Fox"}, {"title": "Wolf"}])
|
||||
prepare_index(service.index_name, documents)
|
||||
mock_rerank.return_value = documents
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
response = APIClient().post(
|
||||
"/api/v1.0/documents/search/",
|
||||
{
|
||||
"q": "programming",
|
||||
"rerank": False,
|
||||
"visited": [doc["id"] for doc in documents],
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert not mock_rerank.called
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Test suite for reranking service
|
||||
"""
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.services.reranking import rerank
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ORIGINAL_HITS = [
|
||||
{
|
||||
"_id": "doc-1",
|
||||
"_score": 1.5,
|
||||
"_source": {
|
||||
"title.en": "Dogs and wolves",
|
||||
"content.en": "Dogs are domesticated wolves",
|
||||
},
|
||||
},
|
||||
{
|
||||
"_id": "doc-2",
|
||||
"_score": 1.2,
|
||||
"_source": {
|
||||
"title.en": "Cats as pets",
|
||||
"content.en": "Cats are popular pets",
|
||||
},
|
||||
},
|
||||
{
|
||||
"_id": "doc-3",
|
||||
"_score": 1.0,
|
||||
"_source": {
|
||||
"title.en": "Birds in nature",
|
||||
"content.en": "Birds fly in the sky",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
RERANKER_RESULTS = [
|
||||
{"doc_id": "doc-2", "score": 0.95},
|
||||
{"doc_id": "doc-1", "score": 0.87},
|
||||
{"doc_id": "doc-3", "score": 0.45},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(name="get_reranker")
|
||||
def mock_get_reranker():
|
||||
"""Mock reranker result with reranked scores"""
|
||||
with patch("core.services.reranking.get_reranker") as mocked_get_reranker:
|
||||
reranker_result = MagicMock()
|
||||
|
||||
reranker_result.results = [
|
||||
MagicMock(**reranker_result) for reranker_result in RERANKER_RESULTS
|
||||
]
|
||||
|
||||
mock_reranker = MagicMock()
|
||||
mock_reranker.rank.return_value = reranker_result
|
||||
mocked_get_reranker.return_value = mock_reranker
|
||||
|
||||
# Yield both the patched function and the mock reranker instance
|
||||
yield mock_reranker
|
||||
|
||||
|
||||
@patch("core.services.reranking.get_reranker")
|
||||
def test_return_original_results_if_reranker_import_fails(mocked_get_reranker, caplog):
|
||||
"""Test that original results are returned if reranker import fails"""
|
||||
mocked_get_reranker.return_value = None
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = rerank("test query", ORIGINAL_HITS)
|
||||
|
||||
assert result == ORIGINAL_HITS
|
||||
assert any(
|
||||
"Could not import reranker, returning original results" in message
|
||||
for message in caplog.messages
|
||||
)
|
||||
|
||||
|
||||
@patch("core.services.reranking.get_reranker")
|
||||
def test_return_original_results_if_reranking_fails(mocked_get_reranker, caplog):
|
||||
"""Test that original results are returned if reranking fails"""
|
||||
mock_reranker = MagicMock()
|
||||
mock_reranker.rank.side_effect = Exception("Reranking service error")
|
||||
mocked_get_reranker.return_value = mock_reranker
|
||||
|
||||
q = "test query"
|
||||
with caplog.at_level(logging.ERROR):
|
||||
result = rerank(q, ORIGINAL_HITS)
|
||||
|
||||
assert result == ORIGINAL_HITS
|
||||
assert any(
|
||||
"Reranking failed: Reranking service error, returning original results"
|
||||
in message
|
||||
for message in caplog.messages
|
||||
)
|
||||
|
||||
|
||||
def test_reranking_success(get_reranker, caplog):
|
||||
"""Test successful reranking of search results"""
|
||||
mock_reranker = get_reranker
|
||||
|
||||
q = "test query"
|
||||
with caplog.at_level(logging.INFO):
|
||||
reranked_hits = rerank(q, ORIGINAL_HITS.copy())
|
||||
|
||||
assert any(
|
||||
f"Reranking 3 results for query: {q}" in message for message in caplog.messages
|
||||
)
|
||||
assert any(
|
||||
"Reranking completed, returned 3 results" in message
|
||||
for message in caplog.messages
|
||||
)
|
||||
|
||||
mock_reranker.rank.assert_called_once()
|
||||
call_args = mock_reranker.rank.call_args
|
||||
assert call_args.kwargs["query"] == q
|
||||
assert len(call_args.kwargs["docs"]) == len(RERANKER_RESULTS)
|
||||
|
||||
for ranked_hit_index, ranked_hit in enumerate(reranked_hits):
|
||||
reranker_result = RERANKER_RESULTS[ranked_hit_index]
|
||||
expected_doc_id = reranker_result["doc_id"]
|
||||
expected_score = reranker_result["score"]
|
||||
expected_reranked_hit = ORIGINAL_HITS[
|
||||
next(
|
||||
_hit_index
|
||||
for _hit_index, hit in enumerate(ORIGINAL_HITS)
|
||||
if hit["_id"] == expected_doc_id
|
||||
)
|
||||
]
|
||||
|
||||
assert expected_doc_id == ranked_hit["_id"]
|
||||
assert expected_reranked_hit["_id"] == ranked_hit["_id"]
|
||||
assert expected_reranked_hit["_source"] == ranked_hit["_source"]
|
||||
assert expected_reranked_hit["_score"] == ranked_hit["_score"]
|
||||
assert expected_score == expected_reranked_hit["_reranked_score"]
|
||||
@@ -5,6 +5,7 @@ Test suite for opensearch search service
|
||||
import logging
|
||||
import operator
|
||||
from json import dumps as json_dumps
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
@@ -712,3 +713,58 @@ def test_search_filtering_by_query_path_and_tag():
|
||||
|
||||
assert result["hits"]["total"]["value"] == len(documents_to_search)
|
||||
assert returned_ids == expected_ids
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||
@responses.activate
|
||||
@patch("core.services.search.rerank")
|
||||
@pytest.mark.parametrize(
|
||||
"rerank_requested, reranker_enabled, rerank_called, caplog_messages",
|
||||
[
|
||||
(True, True, True, []),
|
||||
(
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
[
|
||||
"Reranking was explicitly requested but the reranker is "
|
||||
"disabled in settings. Reranking skipped."
|
||||
],
|
||||
),
|
||||
(False, True, False, []),
|
||||
(False, False, False, []),
|
||||
(None, False, False, []),
|
||||
(None, True, True, []),
|
||||
],
|
||||
)
|
||||
def test_search_rerank( # noqa : PLR0913
|
||||
mock_rerank,
|
||||
rerank_requested,
|
||||
reranker_enabled,
|
||||
rerank_called,
|
||||
caplog_messages,
|
||||
settings,
|
||||
caplog,
|
||||
):
|
||||
"""Test that rerank activation according to args and settings"""
|
||||
settings.RERANKER_ENABLED = reranker_enabled
|
||||
|
||||
documents = bulk_create_documents(
|
||||
[
|
||||
{"title": "wolf", "content": "wolves live in packs and hunt together"},
|
||||
{"title": "dog", "content": "dogs are loyal domestic animals"},
|
||||
{"title": "cat", "content": "cats are curious and independent pets"},
|
||||
]
|
||||
)
|
||||
mock_rerank.return_value = documents
|
||||
service = factories.ServiceFactory(name=SERVICE_NAME)
|
||||
prepare_index(service.index_name, documents)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
search(
|
||||
q="canine pet", rerank_requested=rerank_requested, **search_params(service)
|
||||
)
|
||||
|
||||
assert mock_rerank.called == rerank_called
|
||||
for message in caplog_messages:
|
||||
assert message in caplog.text
|
||||
|
||||
+14
-11
@@ -12,13 +12,14 @@ from rest_framework.response import Response
|
||||
|
||||
from . import schemas
|
||||
from .authentication import ServiceTokenAuthentication
|
||||
from .enums import SearchTypeEnum
|
||||
from .permissions import IsAuthAuthenticated
|
||||
from .services.indexing import (
|
||||
ensure_index_exists,
|
||||
get_opensearch_indices,
|
||||
prepare_document_for_indexing,
|
||||
)
|
||||
from .services.opensearch import opensearch_client
|
||||
from .services.opensearch import check_hybrid_search_enabled, opensearch_client
|
||||
from .services.search import search
|
||||
from .utils import get_language_value
|
||||
|
||||
@@ -305,6 +306,7 @@ class SearchDocumentView(ResourceServerMixin, views.APIView):
|
||||
- The search results can be sorted or filtered via querystring parameters.
|
||||
"""
|
||||
|
||||
authentication_classes = [ResourceServerAuthentication]
|
||||
permission_classes = [IsAuthAuthenticated]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -352,6 +354,9 @@ class SearchDocumentView(ResourceServerMixin, views.APIView):
|
||||
- 'full_text': Uses only full-text search, even if hybrid search is enabled
|
||||
on the server.
|
||||
if the not specified, the server will use hybrid search when enabled
|
||||
rerank : bool, optional
|
||||
Enable or disable reranking of results. If not specified, falls back to
|
||||
the RERANKER_ENABLED setting.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
@@ -362,14 +367,7 @@ class SearchDocumentView(ResourceServerMixin, views.APIView):
|
||||
# Get list of groups related to the user from SCIM provider (consider caching result)
|
||||
audience = self._get_service_provider_audience()
|
||||
user_sub = self.request.user.sub
|
||||
groups = []
|
||||
|
||||
try:
|
||||
params = schemas.SearchQueryParametersSchema(**request.data)
|
||||
except PydanticValidationError as excpt:
|
||||
errors = {error["loc"][0]: error["msg"] for error in excpt.errors()}
|
||||
logger.error("Validation error: %s", errors)
|
||||
raise excpt
|
||||
params = schemas.SearchQueryParametersSchema(**request.data)
|
||||
|
||||
# Get index list for search query
|
||||
try:
|
||||
@@ -391,10 +389,15 @@ class SearchDocumentView(ResourceServerMixin, views.APIView):
|
||||
reach=params.reach,
|
||||
visited=params.visited,
|
||||
user_sub=user_sub,
|
||||
groups=groups,
|
||||
groups=[],
|
||||
tags=params.tags,
|
||||
path=params.path,
|
||||
search_type=params.search_type,
|
||||
search_type=params.search_type
|
||||
if params.search_type
|
||||
else SearchTypeEnum.HYBRID
|
||||
if check_hybrid_search_enabled()
|
||||
else SearchTypeEnum.FULL_TEXT,
|
||||
rerank_requested=params.rerank,
|
||||
)["hits"]["hits"]
|
||||
logger.info("found %d results", len(result))
|
||||
logger.debug("results %s", result)
|
||||
|
||||
@@ -319,6 +319,24 @@ class Base(Configuration):
|
||||
default=1024, environ_name="EMBEDDING_DIMENSION", environ_prefix=None
|
||||
)
|
||||
|
||||
# Reranking
|
||||
RERANKER_ENABLED = values.BooleanValue(
|
||||
default=False, environ_name="RERANKER_ENABLED", environ_prefix=None
|
||||
)
|
||||
RERANKER_MODEL_NAME = values.Value(
|
||||
default=None,
|
||||
environ_name="RERANKER_MODEL_NAME",
|
||||
environ_prefix=None,
|
||||
)
|
||||
RERANKER_MODEL_TYPE = values.Value(
|
||||
default=None,
|
||||
environ_name="RERANKER_MODEL_TYPE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
RERANKER_API_KEY = values.Value(
|
||||
default=None, environ_name="RERANKER_API_KEY", environ_prefix=None
|
||||
)
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True)
|
||||
|
||||
@@ -44,6 +44,7 @@ dependencies = [
|
||||
"pydantic==2.12.5",
|
||||
"pyjwt==2.10.1",
|
||||
"requests==2.32.5",
|
||||
"rerankers[api]==0.10.0",
|
||||
"sentry-sdk==2.48.0",
|
||||
"url-normalize==2.2.1",
|
||||
"opensearch-py==3.1.0",
|
||||
|
||||
Generated
+16
@@ -514,6 +514,7 @@ dependencies = [
|
||||
{ name = "pyjwt" },
|
||||
{ name = "redis" },
|
||||
{ name = "requests" },
|
||||
{ name = "rerankers", extra = ["api"] },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "url-normalize" },
|
||||
{ name = "whitenoise" },
|
||||
@@ -574,6 +575,7 @@ requires-dist = [
|
||||
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" },
|
||||
{ name = "redis", specifier = "==5.3.1" },
|
||||
{ name = "requests", specifier = "==2.32.5" },
|
||||
{ name = "rerankers", extras = ["api"], specifier = "==0.10.0" },
|
||||
{ name = "responses", marker = "extra == 'dev'", specifier = "==0.25.8" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.10" },
|
||||
{ name = "sentry-sdk", specifier = "==2.48.0" },
|
||||
@@ -1417,6 +1419,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rerankers"
|
||||
version = "0.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/1e/3ed2026be7c135939905eac4f50d1bf8339180821c6757b2e91b83de2fa5/rerankers-0.10.0.tar.gz", hash = "sha256:b8e8b363abc4e9757151956949c27b197993c0a774437287a932f855afc17a73", size = 49679, upload-time = "2025-05-22T08:22:53.396Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ed/f3b81ca8743d69b95d679b95e6e1d22cb7cc678ae77c6a57827303a7e48c/rerankers-0.10.0-py3-none-any.whl", hash = "sha256:634a6befa130a245ed46022ade217ee482869448f01aae2051ed54d7d5bd2791", size = 53084, upload-time = "2025-05-22T08:22:52.022Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
api = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "responses"
|
||||
version = "0.25.8"
|
||||
|
||||
Reference in New Issue
Block a user