Make the embedding model swappable via env for A/B testing

Trialing Qwen3-Embedding-0.6B (1024-dim, instruct-prefixed queries)
against nomic required code changes at every hardcoded seam; now it's a
config flip plus a reembed_embeddings run.

- EMBEDDING_DIM env (default 768) replaces every hardcoded dim check:
  daily summary / calendar / search / location DAOs, Ollama batch
  validation, reembed_embeddings
- entities gains the dim guard it never had — a wrong-dim vector
  silently kills dedup/recall (cosine over mismatched lengths is 0),
  so store None and warn instead
- embed_query / embed_document split with EMBED_QUERY_PREFIX /
  EMBED_DOCUMENT_PREFIX (literal \n expanded): retrieval models treat
  the two sides differently — nomic wants search_query:/search_document:,
  Qwen3 wants Instruct:...\nQuery: on queries only. All query-side
  call sites and all corpus writers now declare their side.
- document the contract in CLAUDE.md: change the model or any of these
  vars → re-run reembed_embeddings or search is garbage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-06-11 21:40:40 -04:00
parent b1493f5aca
commit efd05db523
12 changed files with 159 additions and 67 deletions
+13 -10
View File
@@ -222,11 +222,12 @@ impl CalendarEventDao for SqliteCalendarEventDao {
// Validate embedding dimensions if provided
if let Some(ref emb) = event.embedding
&& emb.len() != 768
&& emb.len() != crate::ai::embedding_dim()
{
return Err(anyhow::anyhow!(
"Invalid embedding dimensions: {} (expected 768)",
emb.len()
"Invalid embedding dimensions: {} (expected {})",
emb.len(),
crate::ai::embedding_dim()
));
}
@@ -293,7 +294,7 @@ impl CalendarEventDao for SqliteCalendarEventDao {
for event in events {
// Validate embedding if provided
if let Some(ref emb) = event.embedding
&& emb.len() != 768
&& emb.len() != crate::ai::embedding_dim()
{
log::warn!(
"Skipping event with invalid embedding dimensions: {}",
@@ -385,10 +386,11 @@ impl CalendarEventDao for SqliteCalendarEventDao {
trace_db_call(context, "query", "find_similar_events", |_span| {
let mut conn = self.connection.lock().expect("Unable to get CalendarEventDao");
if query_embedding.len() != 768 {
if query_embedding.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid query embedding dimensions: {} (expected 768)",
query_embedding.len()
"Invalid query embedding dimensions: {} (expected {})",
query_embedding.len(),
crate::ai::embedding_dim()
));
}
@@ -461,10 +463,11 @@ impl CalendarEventDao for SqliteCalendarEventDao {
// Step 2: If query embedding provided, rank by semantic similarity
if let Some(query_emb) = query_embedding {
if query_emb.len() != 768 {
if query_emb.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid query embedding dimensions: {} (expected 768)",
query_emb.len()
"Invalid query embedding dimensions: {} (expected {})",
query_emb.len(),
crate::ai::embedding_dim()
));
}
+12 -9
View File
@@ -150,10 +150,11 @@ impl DailySummaryDao for SqliteDailySummaryDao {
.expect("Unable to get DailySummaryDao");
// Validate embedding dimensions
if summary.embedding.len() != 768 {
if summary.embedding.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid embedding dimensions: {} (expected 768)",
summary.embedding.len()
"Invalid embedding dimensions: {} (expected {})",
summary.embedding.len(),
crate::ai::embedding_dim()
));
}
@@ -202,10 +203,11 @@ impl DailySummaryDao for SqliteDailySummaryDao {
trace_db_call(context, "query", "find_similar_summaries", |_span| {
let mut conn = self.connection.lock().expect("Unable to get DailySummaryDao");
if query_embedding.len() != 768 {
if query_embedding.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid query embedding dimensions: {} (expected 768)",
query_embedding.len()
"Invalid query embedding dimensions: {} (expected {})",
query_embedding.len(),
crate::ai::embedding_dim()
));
}
@@ -299,10 +301,11 @@ impl DailySummaryDao for SqliteDailySummaryDao {
trace_db_call(context, "query", "find_similar_summaries_with_time_weight", |_span| {
let mut conn = self.connection.lock().expect("Unable to get DailySummaryDao");
if query_embedding.len() != 768 {
if query_embedding.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid query embedding dimensions: {} (expected 768)",
query_embedding.len()
"Invalid query embedding dimensions: {} (expected {})",
query_embedding.len(),
crate::ai::embedding_dim()
));
}
+5 -4
View File
@@ -216,11 +216,12 @@ impl LocationHistoryDao for SqliteLocationHistoryDao {
// Validate embedding dimensions if provided (rare for location data)
if let Some(ref emb) = location.embedding
&& emb.len() != 768
&& emb.len() != crate::ai::embedding_dim()
{
return Err(anyhow::anyhow!(
"Invalid embedding dimensions: {} (expected 768)",
emb.len()
"Invalid embedding dimensions: {} (expected {})",
emb.len(),
crate::ai::embedding_dim()
));
}
@@ -292,7 +293,7 @@ impl LocationHistoryDao for SqliteLocationHistoryDao {
for location in locations {
// Validate embedding if provided (rare)
if let Some(ref emb) = location.embedding
&& emb.len() != 768
&& emb.len() != crate::ai::embedding_dim()
{
log::warn!(
"Skipping location with invalid embedding dimensions: {}",
+13 -10
View File
@@ -189,10 +189,11 @@ impl SearchHistoryDao for SqliteSearchHistoryDao {
.expect("Unable to get SearchHistoryDao");
// Validate embedding dimensions (REQUIRED for searches)
if search.embedding.len() != 768 {
if search.embedding.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid embedding dimensions: {} (expected 768)",
search.embedding.len()
"Invalid embedding dimensions: {} (expected {})",
search.embedding.len(),
crate::ai::embedding_dim()
));
}
@@ -245,7 +246,7 @@ impl SearchHistoryDao for SqliteSearchHistoryDao {
conn.transaction::<_, anyhow::Error, _>(|conn| {
for search in searches {
// Validate embedding (REQUIRED)
if search.embedding.len() != 768 {
if search.embedding.len() != crate::ai::embedding_dim() {
log::warn!(
"Skipping search with invalid embedding dimensions: {}",
search.embedding.len()
@@ -325,10 +326,11 @@ impl SearchHistoryDao for SqliteSearchHistoryDao {
.lock()
.expect("Unable to get SearchHistoryDao");
if query_embedding.len() != 768 {
if query_embedding.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid query embedding dimensions: {} (expected 768)",
query_embedding.len()
"Invalid query embedding dimensions: {} (expected {})",
query_embedding.len(),
crate::ai::embedding_dim()
));
}
@@ -406,10 +408,11 @@ impl SearchHistoryDao for SqliteSearchHistoryDao {
// Step 2: If query embedding provided, rank by semantic similarity
if let Some(query_emb) = query_embedding {
if query_emb.len() != 768 {
if query_emb.len() != crate::ai::embedding_dim() {
return Err(anyhow::anyhow!(
"Invalid query embedding dimensions: {} (expected 768)",
query_emb.len()
"Invalid query embedding dimensions: {} (expected {})",
query_emb.len(),
crate::ai::embedding_dim()
));
}