Phase 3 core memory substrate
find_similar read path using VectorStore.search() with HNSW index and GIN full-text fallback when vector results are sparse. EXPLAIN ANALYZE verification of index usage required.
Milestone 3.D.1 — Semantic search read path
Status: Complete
Goal: Track D — Read/Ranking Pipeline
Phase: 3 — Core Memory Substrate
Estimated effort: 2 days
Step 0 — Scoring reconciliation (handoff to 3.D.2)
Before implementing the read path, reconcile composite scoring state:
| Item | Finding |
|---|---|
| Scoring module | services/memory/app/scoring/ (composite.py, half_life.py) |
| Category half-life | Already category-conditional in half_life.py (factual=180d … episodic=14d); multi-label uses shortest half-life |
| 3.2.1 vs 3.D.2 page | 3.2.1 checklist is correct; the 3.D.2 milestone page described a bug that was fixed in Track B. Regression: test_old_factual_outranks_fresh_episodic_at_equal_relevance in test_composite.py |
| 3.D.1 scope | Must not call composite_score() — raw similarity / ts_rank_cd only |
| 3.D.2 remaining | Wire composite into search ranking, optional config-driven half-life table, doc updates |
Ranking state for 3.D.2 (handoff)
| Topic | Current state |
|---|---|
| Ranking today | Vector hits first by raw cosine similarity; FTS append by ts_rank_cd; no composite_score() |
| Score field | MemorySearchResult.similarity holds raw similarity or FTS rank depending on source |
| Source values | "vector" | "full_text" |
| Wire point | After merge/dedupe in repository.py find_similar() — apply composite_score() before final ordering |
| Half-life | Already category-conditional in app/scoring/half_life.py |
| Regression test | test_old_factual_outranks_fresh_episodic_at_equal_relevance in test_composite.py |
| Fallback interaction | Composite scoring applies to merged vector+FTS candidates; preserve dedupe by memory_id |
Delivered
MemoryReadRepository.find_similar()— vector primary, GIN supplemental when sparsePOST /v1/memories/search—memory:read, minimal request (agent_id,query,limit,min_similarity,min_confidence)- Fallback trigger:
len(hydrated_vector_results) < limit— caller-relative, not a fixed constant (ADR-0058) - Merge: vector hits first (desc similarity), FTS append (desc
ts_rank_cd), dedupe by id, cap atlimit - Metric:
ibex_memory_search_fallback_total{triggered}(increments only when FTS returns new candidates) - Toggle:
IBEX_MEMORY_SEARCH_FALLBACK_ENABLED(default true)
Fallback trigger semantics
"Sparse" means the caller's page is unfilled after vector search + hydrate — whether because corpus < limit, min_similarity filtered ANN hits, or min_confidence dropped rows. There is no fixed VECTOR_FALLBACK_THRESHOLD; Phase 3 goals "vector results < K" means caller limit (K), not a magic number.
| Scenario | limit | Condition | FTS fires? |
|---|---|---|---|
| Typical agent | 10 (default) | 15 memories, all pass filters | No |
| Small agent | 10 | 8 memories | Yes |
| Large limit | 20 | 15 memories | Yes |
| Full page | 3 | ≥3 vector hits after hydrate | No |
Latency (integration smoke, 500-row corpus, CI hardware)
- Vector-only
find_similarp95 gate: < 100ms (test_find_similar_latency_smoke; aligns with DEPLOYMENT alert threshold) - Phase 3 HNSW component budget: p95 < 30ms @ 1M vectors (latency table)
- FTS supplement adds a small measured delta on the same smoke test; hard gate is vector-only p95
Schema (no new migration)
Confirmed in 000017_memory_schema_v2_expand.up.sql:
search_vector(generated tsvector) +idx_memories_search_vector(GIN)idx_memories_embedding_hnsw(m=16, ef_construction=64)
Success signals
- Semantic search uses the HNSW path (
pg_stat idx_scanruntime gate intest_hnsw_index_scan_at_runtime) - GIN full-text fallback uses
idx_memories_search_vectorat runtime (test_gin_index_used_at_runtime: productionfull_text_search()+ test-onlyexplain_gin_probe_plan; HNSW usespg_stat idx_scanintest_hnsw_index_scan_at_runtime) - Cross-org result leakage covered by integration isolation test
- Full-text fallback triggers when vector results are sparse
- Search calls are always org/agent scoped — no unscoped queries
- Unit coverage ≥95% on
app/read/(100% at merge)
Prerequisites
- Milestone 3.1.1 merged (HNSW index and
search_vectorGIN index in schema) - Milestone 3.2.1 merged (
VectorStoreABC andPgVectorStoreimplementation available) - Track C milestones (write path in place for test fixtures)
Edit on GitHub
Last updated on