Skip to content

Commit 102f792

Browse files
Yuri ZmytrakovYuri Zmytrakov
authored andcommitted
feat: Add hidden item filtering with HIDE_ITEM_PATH
Adds ability for item hiding via HIDE_ITEM_PATH env var which contains the path to variable on item level specifying if item should be hidden or not. Hidden items are excluded from search results and get_one_item() returns NotFoundError for hidden items. - Updated execute_search() to filter hidden items from search/count queries - Modified get_one_item() to respect hidden status - Uses add_hidden_filter() utility for consistent filtering
1 parent 39fa181 commit 102f792

File tree

4 files changed

+136
-6
lines changed

4 files changed

+136
-6
lines changed

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
add_collections_to_body,
5454
)
5555
from stac_fastapi.sfeos_helpers.database.utils import (
56+
add_hidden_filter,
5657
merge_to_operations,
5758
operations_to_script,
5859
)
@@ -406,12 +407,22 @@ async def get_one_item(self, collection_id: str, item_id: str) -> Dict:
406407
Notes:
407408
The Item is retrieved from the Elasticsearch database using the `client.get` method,
408409
with the index for the Collection as the target index and the combined `mk_item_id` as the document id.
410+
Item is hidden if hide_item_path is configured via env var.
409411
"""
410412
try:
413+
base_query = {"term": {"_id": mk_item_id(item_id, collection_id)}}
414+
415+
HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
416+
417+
if HIDE_ITEM_PATH:
418+
query = add_hidden_filter(base_query, HIDE_ITEM_PATH)
419+
else:
420+
query = base_query
421+
411422
response = await self.client.search(
412423
index=index_alias_by_collection_id(collection_id),
413424
body={
414-
"query": {"term": {"_id": mk_item_id(item_id, collection_id)}},
425+
"query": query,
415426
"size": 1,
416427
},
417428
)
@@ -854,6 +865,10 @@ async def execute_search(
854865

855866
size_limit = min(limit + 1, max_result_window)
856867

868+
HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
869+
if HIDE_ITEM_PATH and query:
870+
query = add_hidden_filter(query, HIDE_ITEM_PATH)
871+
857872
search_task = asyncio.create_task(
858873
self.client.search(
859874
index=index_param,
@@ -865,11 +880,18 @@ async def execute_search(
865880
)
866881
)
867882

883+
# Apply hidden filter to count query as well
884+
count_query = search.to_dict(count=True)
885+
if HIDE_ITEM_PATH and "query" in count_query:
886+
count_query["query"] = add_hidden_filter(
887+
count_query["query"], HIDE_ITEM_PATH
888+
)
889+
868890
count_task = asyncio.create_task(
869891
self.client.count(
870892
index=index_param,
871893
ignore_unavailable=ignore_unavailable,
872-
body=search.to_dict(count=True),
894+
body=count_query,
873895
)
874896
)
875897

stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
add_collections_to_body,
5050
)
5151
from stac_fastapi.sfeos_helpers.database.utils import (
52+
add_hidden_filter,
5253
merge_to_operations,
5354
operations_to_script,
5455
)
@@ -406,12 +407,22 @@ async def get_one_item(self, collection_id: str, item_id: str) -> Dict:
406407
Notes:
407408
The Item is retrieved from the Opensearch database using the `client.get` method,
408409
with the index for the Collection as the target index and the combined `mk_item_id` as the document id.
410+
Item is hidden if hide_item_path is configured via env var.
409411
"""
410412
try:
413+
base_query = {"term": {"_id": mk_item_id(item_id, collection_id)}}
414+
415+
HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
416+
417+
if HIDE_ITEM_PATH:
418+
query = add_hidden_filter(base_query, HIDE_ITEM_PATH)
419+
else:
420+
query = base_query
421+
411422
response = await self.client.search(
412423
index=index_alias_by_collection_id(collection_id),
413424
body={
414-
"query": {"term": {"_id": mk_item_id(item_id, collection_id)}},
425+
"query": query,
415426
"size": 1,
416427
},
417428
)
@@ -846,7 +857,11 @@ async def execute_search(
846857
index_param = ITEM_INDICES
847858
query = add_collections_to_body(collection_ids, query)
848859

849-
if query:
860+
HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
861+
862+
if HIDE_ITEM_PATH:
863+
search_body["query"] = add_hidden_filter(query, HIDE_ITEM_PATH)
864+
elif query:
850865
search_body["query"] = query
851866

852867
search_after = None
@@ -871,11 +886,16 @@ async def execute_search(
871886
)
872887
)
873888

889+
# Ensure hidden item is not counted
890+
count_query = search.to_dict(count=True)
891+
if "query" in count_query:
892+
count_query["query"] = add_hidden_filter(count_query["query"])
893+
874894
count_task = asyncio.create_task(
875895
self.client.count(
876896
index=index_param,
877897
ignore_unavailable=ignore_unavailable,
878-
body=search.to_dict(count=True),
898+
body=count_query,
879899
)
880900
)
881901

stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database/utils.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
import logging
8-
from typing import Any, Dict, List, Union
8+
from typing import Any, Dict, List, Optional, Union
99

1010
from stac_fastapi.core.utilities import bbox2polygon, get_bool_env
1111
from stac_fastapi.extensions.core.transaction.request import (
@@ -361,3 +361,35 @@ def operations_to_script(operations: List, create_nest: bool = False) -> Dict:
361361
"lang": "painless",
362362
"params": params,
363363
}
364+
365+
366+
def add_hidden_filter(
367+
query: Optional[Dict[str, Any]] = None, hide_item_path: Optional[str] = None
368+
) -> Dict[str, Any]:
369+
"""Add hidden filter to a query to exclude hidden items.
370+
371+
Args:
372+
query: Optional Elasticsearch query to combine with hidden filter
373+
hide_item_path: Path to the hidden field (e.g., "properties._private.hidden")
374+
If None or empty, return original query (no filtering)
375+
376+
Returns:
377+
Query with hidden filter applied
378+
"""
379+
if not hide_item_path:
380+
return query or {"match_all": {}}
381+
382+
hidden_filter = {
383+
"bool": {
384+
"should": [
385+
{"term": {hide_item_path: False}},
386+
{"bool": {"must_not": {"exists": {"field": hide_item_path}}}},
387+
],
388+
"minimum_should_match": 1,
389+
}
390+
}
391+
392+
if query:
393+
return {"bool": {"must": [query, hidden_filter]}}
394+
else:
395+
return hidden_filter

stac_fastapi/tests/resources/test_item.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,3 +1163,59 @@ async def test_search_datetime_with_null_datetime(
11631163
await txn_client.delete_collection(test_collection["id"])
11641164
except Exception as e:
11651165
logger.warning(f"Failed to delete collection: {e}")
1166+
1167+
1168+
@pytest.mark.asyncio
1169+
async def test_hidden_item_true(app_client, txn_client, load_test_data):
1170+
"""Test item with hidden=true is filtered out."""
1171+
1172+
os.environ["HIDE_ITEM_PATH"] = "properties._private.hidden"
1173+
1174+
test_collection = load_test_data("test_collection.json")
1175+
test_collection["id"] = "test-collection-hidden-true"
1176+
await create_collection(txn_client, collection=test_collection)
1177+
test_item = load_test_data("test_item.json")
1178+
test_item["collection"] = test_collection["id"]
1179+
test_item["id"] = "hidden-item-true"
1180+
test_item["properties"]["_private"] = {"hidden": True}
1181+
1182+
await create_item(txn_client, test_item)
1183+
1184+
resp = await app_client.get("/search", params={"ids": test_item["id"]})
1185+
assert resp.status_code == 200
1186+
result = resp.json()
1187+
assert len(result["features"]) == 0
1188+
1189+
resp = await app_client.get(
1190+
f"/collections/{test_item['collection']}/items/{test_item['id']}"
1191+
)
1192+
assert resp.status_code == 404
1193+
1194+
1195+
@pytest.mark.asyncio
1196+
async def test_hidden_item_false(app_client, txn_client, load_test_data, monkeypatch):
1197+
"""Test that item with hidden=false is not filtered out."""
1198+
1199+
os.environ["HIDE_ITEM_PATH"] = "properties._private.hidden"
1200+
1201+
test_collection = load_test_data("test_collection.json")
1202+
test_collection["id"] = "test-collection-hidden-false"
1203+
await create_collection(txn_client, collection=test_collection)
1204+
1205+
test_item = load_test_data("test_item.json")
1206+
test_item["id"] = "hidden-item-false"
1207+
test_item["collection"] = test_collection["id"]
1208+
test_item["properties"]["_private"] = {"hidden": False}
1209+
1210+
await create_item(txn_client, test_item)
1211+
await refresh_indices(txn_client)
1212+
1213+
resp = await app_client.get("/search", params={"ids": test_item["id"]})
1214+
assert resp.status_code == 200
1215+
result = resp.json()
1216+
assert len(result["features"]) == 1
1217+
1218+
resp = await app_client.get(
1219+
f"/collections/{test_item['collection']}/items/{test_item['id']}"
1220+
)
1221+
assert resp.status_code == 200

0 commit comments

Comments
 (0)