Filter on path (#53)

* (backend) add path filter

allow search filters on path for Docs compatibility

* (backend) test

I improve existing and add new tests
This commit is contained in:
Charles Englebert
2026-02-09 16:45:18 +01:00
committed by GitHub
parent 033bd42bc4
commit 2c090551c0
7 changed files with 321 additions and 124 deletions
+2 -1
View File
@@ -27,7 +27,8 @@ and this project adheres to
issues if the opensearch database is shared between apps.
- ✨(backend) add tags
- ✨(backend) adapt to conversation RAG
- ✨(backend) add deletion endpoint
- ✨(backend) add deletion endpoint
- ✨(backend) add path filter
## Changed
+1
View File
@@ -114,6 +114,7 @@ class SearchQueryParametersSchema(BaseModel):
visited: StringListParameter = Field(default_factory=list)
reach: Optional[enums.ReachEnum] = None
tags: StringListParameter = Field(default_factory=list)
path: Optional[str] = None
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)
+25
View File
@@ -3,6 +3,7 @@
import logging
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from langchain_text_splitters import RecursiveCharacterTextSplitter
from opensearchpy.exceptions import NotFoundError
@@ -14,6 +15,7 @@ from core.services.opensearch_configuration import (
MAPPINGS,
)
from ..models import Service, get_opensearch_index_name
from .embedding import embed_text
from .opensearch import check_hybrid_search_enabled, opensearch_client
@@ -124,3 +126,26 @@ def detect_language_code(text):
return settings.UNDETERMINED_LANGUAGE_CODE
return detected_code
def get_opensearch_indices(audience, services):
"""
Get OpenSearch indices for the given audience and services.
"""
try:
user_service = Service.objects.get(client_id=audience, is_active=True)
except Service.DoesNotExist as e:
logger.warning("Login failed: No service %s found", audience)
raise SuspiciousOperation("Service is not available") from e
# Find allowed sub-services for this service
allowed_services = set(user_service.services.values_list("name", flat=True))
allowed_services.add(user_service.name)
if services:
available_service = set(services).intersection(allowed_services)
if len(available_service) < len(services):
raise SuspiciousOperation("Some requested services are not available")
return [get_opensearch_index_name(service) for service in allowed_services]
+13 -4
View File
@@ -24,6 +24,7 @@ def search( # noqa : PLR0913
user_sub,
groups,
tags,
path=None,
):
"""Perform an OpenSearch search"""
query = get_query(
@@ -34,6 +35,7 @@ def search( # noqa : PLR0913
user_sub=user_sub,
groups=groups,
tags=tags,
path=path,
)
return opensearch_client().search( # pylint: disable=unexpected-keyword-arg
index=",".join(search_indices),
@@ -49,7 +51,6 @@ def search( # noqa : PLR0913
order_direction=order_direction,
),
"size": nb_results,
# Compute query
"query": query,
},
params=get_params(query_keys=query.keys()),
@@ -61,10 +62,10 @@ def search( # noqa : PLR0913
# pylint: disable=too-many-arguments, too-many-positional-arguments
def get_query( # noqa : PLR0913
q, nb_results, reach, visited, user_sub, groups, tags
q, nb_results, reach, visited, user_sub, groups, tags, path=None
):
"""Build OpenSearch query body based on parameters"""
filter_ = get_filter(reach, visited, user_sub, groups, tags)
filter_ = get_filter(reach, visited, user_sub, groups, tags, path)
if q == "*":
logger.info("Performing match_all query")
@@ -155,7 +156,10 @@ def get_full_text_query(q, filter_):
}
def get_filter(reach, visited, user_sub, groups, tags):
# pylint: disable=too-many-arguments, too-many-positional-arguments
def get_filter( # noqa : PLR0913
reach, visited, user_sub, groups, tags, path=None
):
"""Build OpenSearch filter"""
filters = [
{"term": {"is_active": True}}, # filter out inactive documents
@@ -192,6 +196,11 @@ def get_filter(reach, visited, user_sub, groups, tags):
# logical or: if tags are provided the matching documents should have at least one of them
filters.append({"terms": {"tags": tags}})
# Optional path filter
if path:
# filter documents that start with the provided path
filters.append({"prefix": {"path": path}})
return filters
@@ -100,8 +100,6 @@ def test_api_documents_search_query_unknown_user(settings):
introspect=lambda request, user_info: (404, {}, ""),
)
token = build_authorization_bearer()
service = factories.ServiceFactory()
prepare_index(service.index_name, [])
@@ -109,7 +107,7 @@ def test_api_documents_search_query_unknown_user(settings):
"/api/v1.0/documents/search/",
{"q": "a quick fox"},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 400
@@ -179,7 +177,6 @@ def test_api_documents_search_reached_docs_invalid_parameters(settings):
def test_api_documents_search_match_all(settings):
"""Searching a document with q='*' should match all docs"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -197,7 +194,7 @@ def test_api_documents_search_match_all(settings):
"/api/v1.0/documents/search/",
{"q": "*", "visited": [doc["id"] for doc in documents]},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -284,7 +281,6 @@ def test_api_documents_full_text_search(settings):
Searching a document by its content should work as expected.
"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
service = factories.ServiceFactory()
documents = bulk_create_documents(
@@ -300,7 +296,7 @@ def test_api_documents_full_text_search(settings):
"/api/v1.0/documents/search/",
{"q": "a quick fox", "visited": [doc["id"] for doc in documents]},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -358,7 +354,6 @@ def test_api_documents_full_text_search(settings):
def test_api_documents_hybrid_search(settings):
"""Searching a document by its content should work as expected"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
# hybrid search is enabled by default
enable_hybrid_search(settings)
responses.add(
@@ -382,7 +377,7 @@ def test_api_documents_hybrid_search(settings):
"/api/v1.0/documents/search/",
{"q": "a quick fox", "visited": [doc["id"] for doc in documents]},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -469,7 +464,6 @@ def test_api_documents_hybrid_search(settings):
def test_api_documents_search_ordering_by_fields(settings):
"""It should be possible to order by several fields"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -503,7 +497,7 @@ def test_api_documents_search_ordering_by_fields(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -520,7 +514,6 @@ def test_api_documents_search_ordering_by_fields(settings):
def test_api_documents_search_ordering_by_relevance(settings):
"""It should be possible to order by relevance (score)"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -543,7 +536,7 @@ def test_api_documents_search_ordering_by_relevance(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -560,7 +553,6 @@ def test_api_documents_search_ordering_by_relevance(settings):
def test_api_documents_search_ordering_by_unknown_field(settings):
"""Trying to sort by an unknown field should return a 400 error"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -588,7 +580,7 @@ def test_api_documents_search_ordering_by_unknown_field(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 400
@@ -608,7 +600,6 @@ def test_api_documents_search_ordering_by_unknown_field(settings):
def test_api_documents_search_ordering_by_unknown_direction(settings):
"""Trying to sort with an unknown direction should return a 400 error"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -631,7 +622,7 @@ def test_api_documents_search_ordering_by_unknown_direction(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 400
@@ -648,7 +639,6 @@ def test_api_documents_search_ordering_by_unknown_direction(settings):
def test_api_documents_search_filtering_by_reach(settings):
"""It should be possible to filter results by their reach"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -670,7 +660,7 @@ def test_api_documents_search_filtering_by_reach(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -684,7 +674,6 @@ def test_api_documents_search_filtering_by_reach(settings):
def test_api_documents_search_with_nb_results(settings):
"""nb_size should correctly return results of given size"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -707,7 +696,7 @@ def test_api_documents_search_with_nb_results(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -723,7 +712,7 @@ def test_api_documents_search_with_nb_results(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
data = response.json()
@@ -738,7 +727,7 @@ def test_api_documents_search_with_nb_results(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
data = response.json()
@@ -750,7 +739,6 @@ def test_api_documents_search_with_nb_results(settings):
def test_api_documents_search_nb_results_invalid_parameters(settings):
"""Invalid nb_results parameters should result in a 400 error"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -779,7 +767,7 @@ def test_api_documents_search_nb_results_invalid_parameters(settings):
"/api/v1.0/documents/search/",
{"q": "*", "nb_results": nb_results},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 400
@@ -791,7 +779,6 @@ def test_api_documents_search_nb_results_invalid_parameters(settings):
def test_api_documents_search_nb_results_with_filtering(settings):
"""nb_results should work correctly when combined with filtering by reach"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -816,7 +803,7 @@ def test_api_documents_search_nb_results_with_filtering(settings):
"visited": public_ids,
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
assert [r["_id"] for r in response.json()] == public_ids[0:nb_results]
@@ -826,7 +813,6 @@ def test_api_documents_search_nb_results_with_filtering(settings):
def test_api_documents_search_filtering_by_tags(settings):
"""Test filtering documents by a single tag via API"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -865,7 +851,7 @@ def test_api_documents_search_filtering_by_tags(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
@@ -878,7 +864,6 @@ def test_api_documents_search_filtering_by_tags(settings):
def test_api_documents_search_without_tags_filter(settings):
"""Test that search works normally when no tags filter is provided"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
token = build_authorization_bearer()
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
@@ -912,8 +897,60 @@ def test_api_documents_search_without_tags_filter(settings):
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {token}",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
assert len(response.json()) == 2
@responses.activate
def test_api_documents_search_filtering_by_path(settings):
"""Test filtering documents by path prefix via API"""
setup_oicd_resource_server(responses, settings, sub="user_sub")
responses.add(
responses.POST,
settings.EMBEDDING_API_PATH,
json=albert_embedding_response.response,
status=200,
)
service = factories.ServiceFactory()
documents = bulk_create_documents(
[
{
"title": "Document with tags",
"content": "Tagged document",
"path": "/path/to/doc1",
},
{
"title": "Document without tags",
"content": "Untagged document",
"path": "/path/to/doc2",
},
{
"title": "Document without tags",
"content": "Untagged document",
"path": "other/path/to/doc3",
},
]
)
prepare_index(service.index_name, documents)
path_filter = "/path/to/"
response = APIClient().post(
"/api/v1.0/documents/search/",
{
"q": "*",
"path": path_filter,
"visited": [doc["id"] for doc in documents],
},
format="json",
HTTP_AUTHORIZATION=f"Bearer {build_authorization_bearer()}",
)
assert response.status_code == 200
assert len(response.json()) == 2
for hit in response.json():
assert hit["_source"]["path"].startswith(path_filter)
+203 -63
View File
@@ -367,75 +367,73 @@ def test_search_filtering_by_single_tag():
"""Test filtering documents by a single tag"""
service = factories.ServiceFactory(name=SERVICE_NAME)
documents = bulk_create_documents(
[
{
"title": "Document with python tag",
"content": "This is about Python programming",
"tags": ["python", "programming"],
},
{
"title": "Document with javascript tag",
"content": "This is about JavaScript",
"tags": ["javascript", "programming"],
},
{
"title": "Document with no tags",
"content": "This has no tags",
"tags": [],
},
]
)
documents_to_search = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-search"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-search", "tag-to-filter"]
),
]
documents_to_filter = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-filter"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
),
]
expected_ids = {str(doc["id"]) for doc in documents_to_search}
prepare_index(service.index_name, documents)
prepare_index(service.index_name, documents_to_search + documents_to_filter)
# Search for documents with python tag
result = search(q="*", **{**search_params(service), "tags": ["python"]})
# Search for documents with tag-to-search tag
result = search(q="*", **{**search_params(service), "tags": ["tag-to-search"]})
returned_ids = {hit["_id"] for hit in result["hits"]["hits"]}
assert result["hits"]["total"]["value"] == 1
assert result["hits"]["hits"][0]["_id"] == str(documents[0]["id"])
assert result["hits"]["total"]["value"] == len(documents_to_search)
assert returned_ids == expected_ids
def test_search_filtering_by_multiple_tags():
"""Test filtering documents by multiple tags (OR logic)"""
service = factories.ServiceFactory(name=SERVICE_NAME)
documents = bulk_create_documents(
[
{
"title": "Document with python tag",
"content": "This is about Python programming",
"tags": ["python", "backend"],
},
{
"title": "Document with javascript tag",
"content": "This is about JavaScript",
"tags": ["javascript", "frontend"],
},
{
"title": "Document with java tag",
"content": "This is about Java",
"tags": ["java", "backend"],
},
{
"title": "Document with no tags",
"content": "This has no tags",
"tags": [],
},
]
)
documents_to_search = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-search-1"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-search-1", "tag-to-filter"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-search-2", "tag-to-filter"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-search-1", "tag-to-search-2"]
),
]
documents_to_filter = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], tags=["tag-to-filter"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
),
]
expected_ids = {str(doc["id"]) for doc in documents_to_search}
prepare_index(service.index_name, documents)
prepare_index(service.index_name, documents_to_search + documents_to_filter)
# Search for documents with python OR javascript tags
# Search for documents with tag-to-search-1 OR tag-to-search-2 tags
result = search(
q="*", **{**search_params(service), "tags": ["python", "javascript"]}
q="*",
**{**search_params(service), "tags": ["tag-to-search-1", "tag-to-search-2"]},
)
assert result["hits"]["total"]["value"] == 2
returned_ids = {hit["_id"] for hit in result["hits"]["hits"]}
assert str(documents[0]["id"]) in returned_ids
assert str(documents[1]["id"]) in returned_ids
assert result["hits"]["total"]["value"] == len(documents_to_search)
assert returned_ids == expected_ids
def test_search_no_tags_filter_returns_all():
@@ -444,22 +442,164 @@ def test_search_no_tags_filter_returns_all():
documents = bulk_create_documents(
[
{
"title": "Document with tags",
"content": "Tagged document",
"tags": ["python"],
},
{
"title": "Document without tags",
"content": "Untagged document",
"tags": [],
},
{
"title": "Document with tags",
"content": "Tagged document",
"tags": ["tag-to-search"],
},
]
)
prepare_index(service.index_name, documents)
# Search without tags filter
result = search(q="*", **search_params(service))
assert result["hits"]["total"]["value"] == 2
assert result["hits"]["total"]["value"] == len(documents)
def test_search_filtering_by_tag_and_query():
"""Test filtering documents by both tag and query text"""
service = factories.ServiceFactory(name=SERVICE_NAME)
documents_to_search = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], title="title to search", tags=["tag-to-search"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 1",
tags=["tag-to-search", "tag-to-filter"],
),
]
documents_to_filter = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], title="title to filter", tags=["tag-to-filter"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], title="title to filter 1", tags=["tag-to-filter"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 2",
),
]
expected_ids = {str(doc["id"]) for doc in documents_to_search}
prepare_index(service.index_name, documents_to_search + documents_to_filter)
# Search with both query and tag filter
result = search(q="search", **{**search_params(service), "tags": ["tag-to-search"]})
returned_ids = {hit["_id"] for hit in result["hits"]["hits"]}
assert result["hits"]["total"]["value"] == len(documents_to_search)
assert returned_ids == expected_ids
def test_search_filtering_by_path():
"""Test filtering documents by path prefix"""
service = factories.ServiceFactory(name=SERVICE_NAME)
documents_to_search = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], path="path/to/search/doc-1"
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], path="path/to/search/doc-2"
),
]
documents_to_filter = [
factories.DocumentSchemaFactory.build(
users=["user_sub"], path="path/to/filter/doc-3"
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
),
]
expected_ids = {str(doc["id"]) for doc in documents_to_search}
prepare_index(service.index_name, documents_to_search + documents_to_filter)
path_filter = "path/to/search"
result = search(q="*", **{**search_params(service), "path": path_filter})
returned_ids = {hit["_id"] for hit in result["hits"]["hits"]}
assert result["hits"]["total"]["value"] == len(documents_to_search)
assert returned_ids == expected_ids
def test_search_filtering_by_query_path_and_tag():
"""Test filtering documents by query text, path prefix and tag combined"""
service = factories.ServiceFactory(name=SERVICE_NAME)
documents_to_search = [
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 0",
path="path/to/search-0",
tags=["tag-to-search"],
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 1",
path="path/to/search/doc1",
tags=["tag-to-search", "tag-to-filter"],
),
]
documents_to_filter = [
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to filter",
path="path/to/search/doc-3",
tags=["tag-to-search"],
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 4",
path="path/to/filter/doc-4",
tags=["tag-to-search"],
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 4",
path="path/to/search/doc-4",
),
factories.DocumentSchemaFactory.build(
users=["user_sub"], title="title to search 5", tags=["tag-to-search"]
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 6",
path="path/to/search/doc-6",
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="title to search 7",
),
factories.DocumentSchemaFactory.build(
users=["user_sub"],
title="",
path="path/to/search/doc-8",
tags=["tag-to-search"],
),
]
expected_ids = {str(doc["id"]) for doc in documents_to_search}
prepare_index(service.index_name, documents_to_search + documents_to_filter)
# Search with query, path and tag filters combined
result = search(
q="search",
**{
**search_params(service),
"path": "path/to/search",
"tags": ["tag-to-search"],
},
)
returned_ids = {hit["_id"] for hit in result["hits"]["hits"]}
assert result["hits"]["total"]["value"] == len(documents_to_search)
assert returned_ids == expected_ids
+9 -25
View File
@@ -12,9 +12,12 @@ from rest_framework.response import Response
from . import schemas
from .authentication import ServiceTokenAuthentication
from .models import Service, get_opensearch_index_name
from .permissions import IsAuthAuthenticated
from .services.indexing import ensure_index_exists, prepare_document_for_indexing
from .services.indexing import (
ensure_index_exists,
get_opensearch_indices,
prepare_document_for_indexing,
)
from .services.opensearch import opensearch_client
from .services.search import search
from .utils import get_language_value
@@ -325,6 +328,9 @@ class SearchDocumentView(ResourceServerMixin, views.APIView):
tags : List[str], optional
Filter results based on the 'tags' field. Documents matching any of the
provided tags will be returned.
path : str, optional
Filter results based on the 'path' field. Only documents whose path
starts with the provided value will be returned.
order_by : str, optional
Order results by 'relevance', 'created_at', 'updated_at', or 'size'.
Defaults to 'relevance' if not specified.
@@ -375,31 +381,9 @@ class SearchDocumentView(ResourceServerMixin, views.APIView):
user_sub=user_sub,
groups=groups,
tags=params.tags,
path=params.path,
)["hits"]["hits"]
logger.info("found %d results", len(result))
logger.debug("results %s", result)
return Response(result, status=status.HTTP_200_OK)
def get_opensearch_indices(audience, services):
"""
Get OpenSearch indices for the given audience and services.
"""
try:
user_service = Service.objects.get(client_id=audience, is_active=True)
except Service.DoesNotExist as e:
logger.warning("Login failed: No service %s found", audience)
raise SuspiciousOperation("Service is not available") from e
# Find allowed sub-services for this service
allowed_services = set(user_service.services.values_list("name", flat=True))
allowed_services.add(user_service.name)
if services:
available_service = set(services).intersection(allowed_services)
if len(available_service) < len(services):
raise SuspiciousOperation("Some requested services are not available")
return [get_opensearch_index_name(service) for service in allowed_services]