Skip to content

Commit ce2ae2e

Browse files
committed
Added local-cache
1 parent 8491e0c commit ce2ae2e

4 files changed

Lines changed: 332 additions & 27 deletions

File tree

API.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ Deletes all memories from the database.
358358
**Notes:**
359359
- Clears `dbmem_content`, `dbmem_vault`, and `dbmem_vault_fts`
360360
- Does not delete settings from `dbmem_settings`
361+
- Does not clear the embedding cache (`dbmem_cache`)
361362
- Uses SAVEPOINT transaction for atomicity
362363

363364
**Example:**
@@ -367,6 +368,35 @@ SELECT memory_clear();
367368

368369
---
369370

371+
#### `memory_cache_clear([provider TEXT, model TEXT])`
372+
373+
Clears the embedding cache.
374+
375+
**Parameters:**
376+
| Parameter | Type | Required | Description |
377+
|-----------|------|----------|-------------|
378+
| `provider` | TEXT | No | Provider name to clear cache for |
379+
| `model` | TEXT | No | Model name to clear cache for |
380+
381+
**Returns:** INTEGER - Number of cache entries deleted
382+
383+
**Notes:**
384+
- With 0 arguments: clears the entire embedding cache
385+
- With 2 arguments: clears cache entries for a specific provider/model combination
386+
- The embedding cache stores computed embeddings keyed by (text hash, provider, model) to avoid redundant computation
387+
- Safe to call at any time — does not affect stored memories
388+
389+
**Example:**
390+
```sql
391+
-- Clear entire cache
392+
SELECT memory_cache_clear();
393+
394+
-- Clear cache for a specific provider/model
395+
SELECT memory_cache_clear('openai', 'text-embedding-3-small');
396+
```
397+
398+
---
399+
370400
### `memory_search`
371401

372402
A virtual table for performing hybrid semantic search.
@@ -434,6 +464,7 @@ AND context = 'meetings';
434464
| `text_weight` | REAL | 0.5 | Weight for FTS in scoring |
435465
| `min_score` | REAL | 0.7 | Minimum score threshold for results |
436466
| `update_access` | INTEGER | 1 | Update last_accessed on search |
467+
| `embedding_cache` | INTEGER | 1 | Cache embeddings to avoid redundant computation |
437468

438469
---
439470

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ SELECT memory_set_option('text_weight', 0.4);
184184

185185
-- File processing
186186
SELECT memory_set_option('extensions', 'md,txt,rst'); -- File types to index
187+
188+
-- Embedding cache (enabled by default)
189+
SELECT memory_set_option('embedding_cache', 0); -- Disable cache
190+
SELECT memory_cache_clear(); -- Clear cached embeddings
187191
```
188192

189193
## Memory Management

src/sqlite-memory.c

Lines changed: 172 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ SQLITE_EXTENSION_INIT1
5656
#define DBMEM_SETTINGS_KEY_TEXT_WEIGHT "text_weight"
5757
#define DBMEM_SETTINGS_KEY_MIN_SCORE "min_score"
5858
#define DBMEM_SETTINGS_KEY_UPDATE_ACCESS "update_access"
59+
#define DBMEM_SETTINGS_KEY_EMBEDDING_CACHE "embedding_cache"
5960

6061
// default values from https://docs.openclaw.ai/concepts/memory
6162
#define DEFAULT_CHARS_PER_TOKEN 4 // Approximate number of characters per token (GPT ≈ 4, Claude ≈ 3.5)
@@ -104,7 +105,12 @@ struct dbmem_context {
104105
double text_weight; // Weight of the FTS results during the merge of the result
105106
double min_score; // Minimum score threshold to filter irrelevant results
106107
bool update_access; // Whether to update last_accessed on search
107-
108+
bool embedding_cache; // Enable/disable embedding cache (default: true)
109+
110+
// Cache
111+
float *cache_buffer; // Reusable buffer for cache hits
112+
int cache_buffer_size; // Allocated size in floats
113+
108114
// Runtime state
109115
int64_t counter; // Chunk counter during file processing
110116
uint64_t hash; // Hash of the current text
@@ -242,6 +248,12 @@ static int dbmem_settings_sync (dbmem_context *ctx, const char *key, sqlite3_val
242248
return 0;
243249
}
244250

251+
if (strcasecmp(key, DBMEM_SETTINGS_KEY_EMBEDDING_CACHE) == 0) {
252+
int n = sqlite3_value_int(value);
253+
ctx->embedding_cache = (n > 0) ? 1 : 0;
254+
return 0;
255+
}
256+
245257
if (strcasecmp(key, DBMEM_SETTINGS_KEY_PROVIDER) == 0) {
246258
char *provider = dbmem_strdup((const char *)sqlite3_value_text(value));
247259
if (provider) {
@@ -308,6 +320,10 @@ static int dbmem_database_init (sqlite3 *db) {
308320
rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
309321
if (rc != SQLITE_OK) return rc;
310322

323+
sql = "CREATE TABLE IF NOT EXISTS dbmem_cache (text_hash INTEGER NOT NULL, provider TEXT NOT NULL, model TEXT NOT NULL, embedding BLOB NOT NULL, dimension INTEGER NOT NULL, PRIMARY KEY (text_hash, provider, model));";
324+
rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
325+
if (rc != SQLITE_OK) return rc;
326+
311327
sql = "CREATE VIRTUAL TABLE IF NOT EXISTS dbmem_vault_fts USING fts5 (content, hash UNINDEXED, seq UNINDEXED, context UNINDEXED);";
312328
rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
313329
if (rc != SQLITE_OK) {
@@ -514,6 +530,7 @@ static void *dbmem_context_create (sqlite3 *db) {
514530
ctx->text_weight = DEFAULT_TEXT_WEIGHT;
515531
ctx->min_score = DEFAULT_MIN_SCORE;
516532
ctx->update_access = true;
533+
ctx->embedding_cache = true;
517534

518535
return (void *)ctx;
519536
}
@@ -526,6 +543,7 @@ static void dbmem_context_free (void *ptr) {
526543
if (ctx->model) dbmem_free(ctx->model);
527544
if (ctx->api_key) dbmem_free(ctx->api_key);
528545
if (ctx->extensions) dbmem_free(ctx->extensions);
546+
if (ctx->cache_buffer) dbmem_free(ctx->cache_buffer);
529547

530548
#ifndef DBMEM_OMIT_LOCAL_ENGINE
531549
if (ctx->l_engine) dbmem_local_engine_free(ctx->l_engine);
@@ -776,6 +794,44 @@ static void dbmem_clear (sqlite3_context *context, int argc, sqlite3_value **arg
776794
sqlite3_result_error(context, sqlite3_errmsg(db), -1);
777795
}
778796

797+
// MARK: - Cache Clear -
798+
799+
static void dbmem_cache_clear (sqlite3_context *context, int argc, sqlite3_value **argv) {
800+
sqlite3 *db = sqlite3_context_db_handle(context);
801+
int rc;
802+
803+
if (argc == 0) {
804+
rc = sqlite3_exec(db, "DELETE FROM dbmem_cache;", NULL, NULL, NULL);
805+
} else if (argc == 2) {
806+
if (sqlite3_value_type(argv[0]) != SQLITE_TEXT || sqlite3_value_type(argv[1]) != SQLITE_TEXT) {
807+
sqlite3_result_error(context, "The function memory_cache_clear expects two arguments of type TEXT (provider, model)", SQLITE_ERROR);
808+
return;
809+
}
810+
const char *provider = (const char *)sqlite3_value_text(argv[0]);
811+
const char *model = (const char *)sqlite3_value_text(argv[1]);
812+
813+
sqlite3_stmt *vm = NULL;
814+
rc = sqlite3_prepare_v2(db, "DELETE FROM dbmem_cache WHERE provider=?1 AND model=?2;", -1, &vm, NULL);
815+
if (rc == SQLITE_OK) {
816+
sqlite3_bind_text(vm, 1, provider, -1, SQLITE_STATIC);
817+
sqlite3_bind_text(vm, 2, model, -1, SQLITE_STATIC);
818+
rc = sqlite3_step(vm);
819+
if (rc == SQLITE_DONE) rc = SQLITE_OK;
820+
}
821+
if (vm) sqlite3_finalize(vm);
822+
} else {
823+
sqlite3_result_error(context, "The function memory_cache_clear expects 0 or 2 arguments", SQLITE_ERROR);
824+
return;
825+
}
826+
827+
if (rc != SQLITE_OK) {
828+
sqlite3_result_error(context, sqlite3_errmsg(db), -1);
829+
return;
830+
}
831+
832+
sqlite3_result_int(context, sqlite3_changes(db));
833+
}
834+
779835
// MARK: - General -
780836

781837
static void dbmem_version (sqlite3_context *context, int argc, sqlite3_value **argv) {
@@ -970,40 +1026,123 @@ static void dbmem_dump_embeding (const embedding_result_t *result) {
9701026
}
9711027
#endif
9721028

1029+
// MARK: - Embedding Cache -
1030+
1031+
static bool dbmem_cache_lookup (dbmem_context *ctx, uint64_t text_hash, embedding_result_t *result) {
1032+
static const char *sql = "SELECT embedding, dimension FROM dbmem_cache WHERE text_hash=?1 AND provider=?2 AND model=?3 LIMIT 1;";
1033+
1034+
if (!ctx->provider || !ctx->model) return false;
1035+
1036+
bool found = false;
1037+
sqlite3_stmt *vm = NULL;
1038+
int rc = sqlite3_prepare_v2(ctx->db, sql, -1, &vm, NULL);
1039+
if (rc != SQLITE_OK) goto cleanup;
1040+
1041+
sqlite3_bind_int64(vm, 1, (sqlite3_int64)text_hash);
1042+
sqlite3_bind_text(vm, 2, ctx->provider, -1, SQLITE_STATIC);
1043+
sqlite3_bind_text(vm, 3, ctx->model, -1, SQLITE_STATIC);
1044+
1045+
rc = sqlite3_step(vm);
1046+
if (rc != SQLITE_ROW) goto cleanup;
1047+
1048+
int dimension = sqlite3_column_int(vm, 1);
1049+
int blob_bytes = sqlite3_column_bytes(vm, 0);
1050+
const void *blob = sqlite3_column_blob(vm, 0);
1051+
1052+
if (blob_bytes != dimension * (int)sizeof(float)) goto cleanup;
1053+
1054+
// ensure cache_buffer is large enough
1055+
if (ctx->cache_buffer_size < dimension) {
1056+
float *new_buf = (float *)dbmem_realloc(ctx->cache_buffer, dimension * sizeof(float));
1057+
if (!new_buf) goto cleanup;
1058+
ctx->cache_buffer = new_buf;
1059+
ctx->cache_buffer_size = dimension;
1060+
}
1061+
1062+
memcpy(ctx->cache_buffer, blob, blob_bytes);
1063+
result->embedding = ctx->cache_buffer;
1064+
result->n_embd = dimension;
1065+
result->n_tokens = 0;
1066+
result->n_tokens_truncated = 0;
1067+
found = true;
1068+
1069+
cleanup:
1070+
if (vm) sqlite3_finalize(vm);
1071+
return found;
1072+
}
1073+
1074+
static void dbmem_cache_store (dbmem_context *ctx, uint64_t text_hash, const embedding_result_t *result) {
1075+
static const char *sql = "INSERT OR REPLACE INTO dbmem_cache (text_hash, provider, model, embedding, dimension) VALUES (?1, ?2, ?3, ?4, ?5);";
1076+
1077+
if (!ctx->provider || !ctx->model) return;
1078+
1079+
sqlite3_stmt *vm = NULL;
1080+
int rc = sqlite3_prepare_v2(ctx->db, sql, -1, &vm, NULL);
1081+
if (rc != SQLITE_OK) goto cleanup;
1082+
1083+
sqlite3_bind_int64(vm, 1, (sqlite3_int64)text_hash);
1084+
sqlite3_bind_text(vm, 2, ctx->provider, -1, SQLITE_STATIC);
1085+
sqlite3_bind_text(vm, 3, ctx->model, -1, SQLITE_STATIC);
1086+
sqlite3_bind_blob(vm, 4, result->embedding, result->n_embd * (int)sizeof(float), SQLITE_STATIC);
1087+
sqlite3_bind_int(vm, 5, result->n_embd);
1088+
1089+
sqlite3_step(vm);
1090+
1091+
cleanup:
1092+
if (vm) sqlite3_finalize(vm);
1093+
}
1094+
1095+
// MARK: -
1096+
9731097
static int dbmem_process_callback (const char *text, size_t len, size_t offset, size_t length, void *xdata, size_t index) {
9741098
dbmem_context *ctx = (dbmem_context *)xdata;
9751099
embedding_result_t result = {0};
9761100
int rc = SQLITE_OK;
977-
978-
// compute embedding
979-
if (ctx->is_local) {
980-
#ifndef DBMEM_OMIT_LOCAL_ENGINE
981-
rc = dbmem_local_compute_embedding(ctx->l_engine, text, (int)len, &result);
982-
if (rc != 0) {
983-
const char *err = dbmem_local_errmsg(ctx->l_engine);
1101+
1102+
// check embedding cache
1103+
uint64_t chunk_hash = 0;
1104+
bool cache_hit = false;
1105+
if (ctx->embedding_cache) {
1106+
chunk_hash = dbmem_hash_compute(text, len);
1107+
cache_hit = dbmem_cache_lookup(ctx, chunk_hash, &result);
1108+
}
1109+
1110+
if (!cache_hit) {
1111+
// compute embedding
1112+
if (ctx->is_local) {
1113+
#ifndef DBMEM_OMIT_LOCAL_ENGINE
1114+
rc = dbmem_local_compute_embedding(ctx->l_engine, text, (int)len, &result);
1115+
if (rc != 0) {
1116+
const char *err = dbmem_local_errmsg(ctx->l_engine);
1117+
memcpy(ctx->error_msg, err, strlen(err) + 1);
1118+
return rc;
1119+
}
1120+
#else
1121+
const char *err = "Local embedding cannot be computed because extension was compiled without local engine support";
9841122
memcpy(ctx->error_msg, err, strlen(err) + 1);
985-
return rc;
1123+
return 1;
1124+
#endif
9861125
}
987-
#else
988-
const char *err = "Local embedding cannot be computed because extension was compiled without local engine support";
989-
memcpy(ctx->error_msg, err, strlen(err) + 1);
990-
return 1;
991-
#endif
992-
}
993-
994-
if (!ctx->is_local) {
995-
#ifndef DBMEM_OMIT_REMOTE_ENGINE
996-
rc = dbmem_remote_compute_embedding(ctx->r_engine, text, (int)len, &result);
997-
if (rc != 0) {
998-
const char *err = dbmem_remote_errmsg(ctx->r_engine);
1126+
1127+
if (!ctx->is_local) {
1128+
#ifndef DBMEM_OMIT_REMOTE_ENGINE
1129+
rc = dbmem_remote_compute_embedding(ctx->r_engine, text, (int)len, &result);
1130+
if (rc != 0) {
1131+
const char *err = dbmem_remote_errmsg(ctx->r_engine);
1132+
memcpy(ctx->error_msg, err, strlen(err) + 1);
1133+
return rc;
1134+
}
1135+
#else
1136+
const char *err = "Remote embedding cannot be computed because extension was compiled without remote engine support";
9991137
memcpy(ctx->error_msg, err, strlen(err) + 1);
1000-
return rc;
1138+
return 1;
1139+
#endif
1140+
}
1141+
1142+
// store in cache on miss
1143+
if (ctx->embedding_cache) {
1144+
dbmem_cache_store(ctx, chunk_hash, &result);
10011145
}
1002-
#else
1003-
const char *err = "Remote embedding cannot be computed because extension was compiled without remote engine support";
1004-
memcpy(ctx->error_msg, err, strlen(err) + 1);
1005-
return 1;
1006-
#endif
10071146
}
10081147

10091148
// make sure dimension is the same
@@ -1283,6 +1422,12 @@ SQLITE_DBMEMORY_API int sqlite3_memory_init (sqlite3 *db, char **pzErrMsg, const
12831422
rc = sqlite3_create_function_v2(db, "memory_clear", 0, SQLITE_UTF8, ctx, dbmem_clear, NULL, NULL, NULL);
12841423
if (rc != SQLITE_OK) return rc;
12851424

1425+
rc = sqlite3_create_function_v2(db, "memory_cache_clear", 0, SQLITE_UTF8, ctx, dbmem_cache_clear, NULL, NULL, NULL);
1426+
if (rc != SQLITE_OK) return rc;
1427+
1428+
rc = sqlite3_create_function_v2(db, "memory_cache_clear", 2, SQLITE_UTF8, ctx, dbmem_cache_clear, NULL, NULL, NULL);
1429+
if (rc != SQLITE_OK) return rc;
1430+
12861431
rc = dbmem_register_search(db, ctx, pzErrMsg);
12871432
if (rc != SQLITE_OK) return rc;
12881433

0 commit comments

Comments
 (0)