Skip to content

Commit 2f34747

Browse files
committed
perf(core): make postgres vector sync tuning configurable
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent d9ab2a1 commit 2f34747

File tree

4 files changed

+34
-9
lines changed

4 files changed

+34
-9
lines changed

src/basic_memory/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ class BasicMemoryConfig(BaseSettings):
198198
description="Batch size for vector sync orchestration flushes.",
199199
gt=0,
200200
)
201+
semantic_postgres_prepare_concurrency: int = Field(
202+
default=4,
203+
description="Number of Postgres entity prepare tasks to run concurrently during vector sync. Postgres only; keep this low to avoid overdriving the database connection pool.",
204+
gt=0,
205+
le=16,
206+
)
201207
semantic_embedding_cache_dir: str | None = Field(
202208
default=None,
203209
description="Optional cache directory for FastEmbed model artifacts.",

src/basic_memory/repository/postgres_search_repository.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828
from basic_memory.schemas.search import SearchItemType, SearchRetrievalMode
2929

3030

31-
POSTGRES_VECTOR_PREPARE_CONCURRENCY = 4
32-
33-
3431
def _strip_nul_from_row(row_data: dict) -> dict:
3532
"""Strip NUL bytes from all string values in a row dict.
3633
@@ -71,6 +68,9 @@ def __init__(
7168
self._semantic_embedding_sync_batch_size = (
7269
self._app_config.semantic_embedding_sync_batch_size
7370
)
71+
self._semantic_postgres_prepare_concurrency = (
72+
self._app_config.semantic_postgres_prepare_concurrency
73+
)
7474
self._embedding_provider = embedding_provider
7575
self._vector_dimensions = 384
7676
self._vector_tables_initialized = False
@@ -503,8 +503,8 @@ async def sync_entity_vectors_batch(
503503
504504
Trigger: cloud indexing uses Neon Postgres where network latency dominates
505505
thousands of per-entity prepare queries.
506-
Why: preparing a small window of entities concurrently hides round-trip latency
507-
without exhausting the tenant connection pool.
506+
Why: preparing a small config-driven window of entities concurrently hides
507+
round-trip latency without exhausting the tenant connection pool.
508508
Outcome: Postgres vector sync keeps the existing flush semantics while reducing
509509
wall-clock time on large cloud projects.
510510
"""
@@ -527,7 +527,7 @@ async def sync_entity_vectors_batch(
527527
project_id=self.project_id,
528528
entities_total=total_entities,
529529
sync_batch_size=self._semantic_embedding_sync_batch_size,
530-
prepare_concurrency=POSTGRES_VECTOR_PREPARE_CONCURRENCY,
530+
prepare_concurrency=self._semantic_postgres_prepare_concurrency,
531531
)
532532

533533
pending_jobs: list[_PendingEmbeddingJob] = []
@@ -536,9 +536,9 @@ async def sync_entity_vectors_batch(
536536
deferred_entity_ids: set[int] = set()
537537
synced_entity_ids: set[int] = set()
538538

539-
for window_start in range(0, total_entities, POSTGRES_VECTOR_PREPARE_CONCURRENCY):
539+
for window_start in range(0, total_entities, self._semantic_postgres_prepare_concurrency):
540540
window_entity_ids = entity_ids[
541-
window_start : window_start + POSTGRES_VECTOR_PREPARE_CONCURRENCY
541+
window_start : window_start + self._semantic_postgres_prepare_concurrency
542542
]
543543

544544
if progress_callback is not None:

tests/repository/test_postgres_search_repository_unit.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def _make_repo(
4040
*,
4141
semantic_enabled: bool = False,
4242
embedding_provider=None,
43+
semantic_postgres_prepare_concurrency: int = 4,
4344
) -> PostgresSearchRepository:
4445
"""Build a PostgresSearchRepository with a no-op session maker."""
4546
session_maker = MagicMock()
@@ -49,6 +50,7 @@ def _make_repo(
4950
default_project="test-project",
5051
database_backend=DatabaseBackend.POSTGRES,
5152
semantic_search_enabled=semantic_enabled,
53+
semantic_postgres_prepare_concurrency=semantic_postgres_prepare_concurrency,
5254
)
5355
return PostgresSearchRepository(
5456
session_maker,
@@ -255,6 +257,7 @@ async def test_sync_entity_vectors_batch_prepares_entities_concurrently(self, mo
255257
repo = _make_repo(
256258
semantic_enabled=True,
257259
embedding_provider=StubEmbeddingProvider(),
260+
semantic_postgres_prepare_concurrency=2,
258261
)
259262
repo._semantic_embedding_sync_batch_size = 8
260263
repo._vector_tables_initialized = True
@@ -283,7 +286,7 @@ async def _stub_prepare(entity_id: int) -> _PreparedEntityVectorSync:
283286
assert result.entities_total == 4
284287
assert result.entities_synced == 4
285288
assert result.entities_failed == 0
286-
assert max_active_prepares > 1
289+
assert max_active_prepares == 2
287290

288291

289292
@pytest.mark.asyncio

tests/test_config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,22 @@ def test_semantic_embedding_dimensions_can_be_set(self):
882882
config = BasicMemoryConfig(semantic_embedding_dimensions=1536)
883883
assert config.semantic_embedding_dimensions == 1536
884884

885+
def test_semantic_postgres_prepare_concurrency_defaults_to_4(self):
886+
"""Postgres prepare concurrency should default to a conservative window of 4."""
887+
config = BasicMemoryConfig()
888+
assert config.semantic_postgres_prepare_concurrency == 4
889+
890+
def test_semantic_postgres_prepare_concurrency_validation(self):
891+
"""Postgres prepare concurrency must stay within the bounded safe range."""
892+
config = BasicMemoryConfig(semantic_postgres_prepare_concurrency=8)
893+
assert config.semantic_postgres_prepare_concurrency == 8
894+
895+
with pytest.raises(Exception):
896+
BasicMemoryConfig(semantic_postgres_prepare_concurrency=0)
897+
898+
with pytest.raises(Exception):
899+
BasicMemoryConfig(semantic_postgres_prepare_concurrency=17)
900+
885901
def test_semantic_search_enabled_description_mentions_both_backends(self):
886902
"""Description should not say 'SQLite only' anymore."""
887903
field_info = BasicMemoryConfig.model_fields["semantic_search_enabled"]

0 commit comments

Comments
 (0)