Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/basic_memory/repository/postgres_search_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,8 @@ async def _build_fts_query_parts(
# Handle date filter
if after_date:
params["after_date"] = after_date
conditions.append("search_index.created_at > :after_date")
# Filter on updated_at so recently-edited notes are included even when created_at is old
conditions.append("search_index.updated_at > :after_date")
# order by most recent first
order_by_clause = ", search_index.updated_at DESC"

Expand Down Expand Up @@ -945,7 +946,7 @@ async def search(
{score_expr} as score
FROM {from_clause}
WHERE {where_clause}
ORDER BY score DESC, search_index.id ASC {order_by_clause}
ORDER BY score DESC {order_by_clause}, search_index.id ASC
LIMIT :limit
OFFSET :offset
"""
Expand Down
3 changes: 2 additions & 1 deletion src/basic_memory/repository/sqlite_search_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,8 @@ async def _build_fts_query_parts(
# Handle date filter using datetime() for proper comparison
if after_date:
params["after_date"] = after_date
conditions.append("datetime(search_index.created_at) > datetime(:after_date)")
# Filter on updated_at so recently-edited notes are included even when created_at is old
conditions.append("datetime(search_index.updated_at) > datetime(:after_date)")

# order by most recent first
order_by_clause = ", search_index.updated_at DESC"
Expand Down
77 changes: 71 additions & 6 deletions tests/services/test_search_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for search service."""

from datetime import datetime
from datetime import datetime, timezone

import pytest
from sqlalchemy import text
Expand Down Expand Up @@ -174,12 +174,12 @@ async def test_after_date(search_service, test_graph):
)
for r in results:
# Handle both string (SQLite) and datetime (Postgres) formats
created_at = (
r.created_at
if isinstance(r.created_at, datetime)
else datetime.fromisoformat(r.created_at)
updated_at = (
r.updated_at
if isinstance(r.updated_at, datetime)
else datetime.fromisoformat(r.updated_at)
)
assert created_at > past_date
assert updated_at > past_date

# Should not find with future date
future_date = datetime(2030, 1, 1).astimezone()
Expand All @@ -192,6 +192,71 @@ async def test_after_date(search_service, test_graph):
assert len(results) == 0


@pytest.mark.asyncio
async def test_after_date_uses_updated_at(search_service):
"""Regression: after_date should filter on updated_at, not created_at.

An entity created before the timeframe but updated within it must appear
in recent-activity results. A stale entity (updated_at also old) must not.
"""
cutoff = datetime(2020, 1, 1, tzinfo=timezone.utc)
old_created = datetime(2015, 6, 1, tzinfo=timezone.utc)
recently_updated = datetime(2023, 3, 15, tzinfo=timezone.utc)
stale_updated = datetime(2018, 6, 1, tzinfo=timezone.utc)

project_id = search_service.repository.project_id

# Leave metadata at its None default — SearchIndexRow.to_insert only
# JSON-serializes truthy metadata, so passing {} would slip an
# un-serialized dict into the SQLite bind and raise ProgrammingError.
recently_updated_row = SearchIndexRow(
project_id=project_id,
id=99001,
type="entity",
file_path="test/recently_updated.md",
title="Recently Updated Entity",
content_snippet="recently updated content",
permalink="test/recently-updated-entity",
created_at=old_created,
updated_at=recently_updated,
)
stale_row = SearchIndexRow(
project_id=project_id,
id=99002,
type="entity",
file_path="test/stale.md",
title="Stale Entity",
content_snippet="stale content",
permalink="test/stale-entity",
created_at=old_created,
updated_at=stale_updated,
)

await search_service.repository.index_item(recently_updated_row)
await search_service.repository.index_item(stale_row)

results = await search_service.search(
SearchQuery(after_date=cutoff.isoformat())
)

permalinks = {r.permalink for r in results}
# recently-updated entity must appear despite old created_at
assert "test/recently-updated-entity" in permalinks
# stale entity must not appear (updated_at is before cutoff)
assert "test/stale-entity" not in permalinks

# results should be ordered newest updated_at first
updated_ats = []
for r in results:
ua = (
r.updated_at
if isinstance(r.updated_at, datetime)
else datetime.fromisoformat(r.updated_at)
)
updated_ats.append(ua.replace(tzinfo=timezone.utc) if ua.tzinfo is None else ua)
assert updated_ats == sorted(updated_ats, reverse=True)


@pytest.mark.asyncio
async def test_search_type(search_service, test_graph):
"""Test search filters."""
Expand Down
Loading