Skip to content

Commit 8ab9114

Browse files
committed
Added cache eviction and oversampling
1 parent d864f4b commit 8ab9114

6 files changed

Lines changed: 194 additions & 5 deletions

File tree

API.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ AND context = 'meetings';
465465
| `min_score` | REAL | 0.7 | Minimum score threshold for results |
466466
| `update_access` | INTEGER | 1 | Update last_accessed on search |
467467
| `embedding_cache` | INTEGER | 1 | Cache embeddings to avoid redundant computation |
468+
| `cache_max_entries` | INTEGER | 0 | Max cache entries (0 = no limit). When exceeded, oldest entries are evicted |
469+
| `search_oversample` | INTEGER | 0 | Search oversampling multiplier (0 = no oversampling). When set, retrieves N * multiplier candidates from each index before merging down to N final results |
468470

469471
---
470472

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,14 @@ SELECT memory_set_option('max_results', 30); -- Max search results
182182
SELECT memory_set_option('min_score', 0.75); -- Score threshold
183183
SELECT memory_set_option('vector_weight', 0.6); -- Vector vs FTS balance
184184
SELECT memory_set_option('text_weight', 0.4);
185+
SELECT memory_set_option('search_oversample', 4); -- Fetch 4x candidates before merging
185186

186187
-- File processing
187188
SELECT memory_set_option('extensions', 'md,txt,rst'); -- File types to index
188189

189190
-- Embedding cache (enabled by default)
190191
SELECT memory_set_option('embedding_cache', 0); -- Disable cache
192+
SELECT memory_set_option('cache_max_entries', 10000); -- Limit cache size (0 = no limit)
191193
SELECT memory_cache_clear(); -- Clear cached embeddings
192194
```
193195

src/dbmem-search.c

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -584,10 +584,14 @@ static int vMemorySearchCursorFilter (sqlite3_vtab_cursor *cur, int idxNum, cons
584584
}
585585
}
586586

587+
// compute fetch count (oversampling)
588+
int oversample = dbmem_context_search_oversample(ctx);
589+
int fetch_count = (oversample > 0) ? max_results * oversample : max_results;
590+
587591
// allocate internal cursor buffer
588-
int rc = vMemorySearchCursorAllocate(c, max_results, perform_fts);
592+
int rc = vMemorySearchCursorAllocate(c, fetch_count, perform_fts);
589593
if (rc != SQLITE_OK) return SQLITE_NOMEM;
590-
594+
591595
// perform semantic search
592596
// retrieve engine
593597
bool is_local;
@@ -627,16 +631,16 @@ static int vMemorySearchCursorFilter (sqlite3_vtab_cursor *cur, int idxNum, cons
627631
}
628632

629633
// perform search
630-
rc = dbmem_semantic_search(db, c, result.embedding, (int)(result.n_embd * sizeof(float)), context, max_results);
634+
rc = dbmem_semantic_search(db, c, result.embedding, (int)(result.n_embd * sizeof(float)), context, fetch_count);
631635
if (rc != 0) {
632636
sqlvTab->zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(db));
633637
return SQLITE_ERROR;
634638
}
635-
639+
636640
// perform fts search
637641
if (perform_fts) {
638642
// in case of FTS error ignore its contribution
639-
rc = dbmem_fts_search(db, c, query, context, max_results);
643+
rc = dbmem_fts_search(db, c, query, context, fetch_count);
640644
if (rc != SQLITE_OK) perform_fts = false;
641645
}
642646

src/sqlite-memory.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ SQLITE_EXTENSION_INIT1
5757
#define DBMEM_SETTINGS_KEY_MIN_SCORE "min_score"
5858
#define DBMEM_SETTINGS_KEY_UPDATE_ACCESS "update_access"
5959
#define DBMEM_SETTINGS_KEY_EMBEDDING_CACHE "embedding_cache"
60+
#define DBMEM_SETTINGS_KEY_CACHE_MAX_ENTRIES "cache_max_entries"
61+
#define DBMEM_SETTINGS_KEY_SEARCH_OVERSAMPLE "search_oversample"
6062

6163
// default values from https://docs.openclaw.ai/concepts/memory
6264
#define DEFAULT_CHARS_PER_TOKEN 4 // Approximate number of characters per token (GPT ≈ 4, Claude ≈ 3.5)
@@ -106,6 +108,8 @@ struct dbmem_context {
106108
double min_score; // Minimum score threshold to filter irrelevant results
107109
bool update_access; // Whether to update last_accessed on search
108110
bool embedding_cache; // Enable/disable embedding cache (default: true)
111+
int cache_max_entries; // Max cache entries (0 = no limit)
112+
int search_oversample; // Search oversampling multiplier (0 = no oversampling)
109113

110114
// Cache
111115
float *cache_buffer; // Reusable buffer for cache hits
@@ -254,6 +258,18 @@ static int dbmem_settings_sync (dbmem_context *ctx, const char *key, sqlite3_val
254258
return 0;
255259
}
256260

261+
if (strcasecmp(key, DBMEM_SETTINGS_KEY_CACHE_MAX_ENTRIES) == 0) {
262+
int n = sqlite3_value_int(value);
263+
if (n >= 0) ctx->cache_max_entries = n;
264+
return 0;
265+
}
266+
267+
if (strcasecmp(key, DBMEM_SETTINGS_KEY_SEARCH_OVERSAMPLE) == 0) {
268+
int n = sqlite3_value_int(value);
269+
if (n >= 0) ctx->search_oversample = n;
270+
return 0;
271+
}
272+
257273
if (strcasecmp(key, DBMEM_SETTINGS_KEY_PROVIDER) == 0) {
258274
char *provider = dbmem_strdup((const char *)sqlite3_value_text(value));
259275
if (provider) {
@@ -644,6 +660,10 @@ bool dbmem_context_update_access (dbmem_context *ctx) {
644660
return ctx->update_access;
645661
}
646662

663+
int dbmem_context_search_oversample (dbmem_context *ctx) {
664+
return ctx->search_oversample;
665+
}
666+
647667
const char *dbmem_context_errmsg (dbmem_context *ctx) {
648668
return ctx->error_msg;
649669
}
@@ -1071,6 +1091,33 @@ static bool dbmem_cache_lookup (dbmem_context *ctx, uint64_t text_hash, embeddin
10711091
return found;
10721092
}
10731093

1094+
static void dbmem_cache_evict (dbmem_context *ctx) {
1095+
static const char *sql = "DELETE FROM dbmem_cache WHERE rowid IN (SELECT rowid FROM dbmem_cache ORDER BY rowid ASC LIMIT ?1);";
1096+
1097+
sqlite3_stmt *vm = NULL;
1098+
int rc = sqlite3_prepare_v2(ctx->db, sql, -1, &vm, NULL);
1099+
if (rc != SQLITE_OK) goto cleanup;
1100+
1101+
// count current entries
1102+
sqlite3_stmt *count_vm = NULL;
1103+
rc = sqlite3_prepare_v2(ctx->db, "SELECT COUNT(*) FROM dbmem_cache;", -1, &count_vm, NULL);
1104+
if (rc != SQLITE_OK) goto cleanup;
1105+
1106+
rc = sqlite3_step(count_vm);
1107+
if (rc != SQLITE_ROW) { sqlite3_finalize(count_vm); goto cleanup; }
1108+
int count = sqlite3_column_int(count_vm, 0);
1109+
sqlite3_finalize(count_vm);
1110+
1111+
int excess = count - ctx->cache_max_entries;
1112+
if (excess <= 0) goto cleanup;
1113+
1114+
sqlite3_bind_int(vm, 1, excess);
1115+
sqlite3_step(vm);
1116+
1117+
cleanup:
1118+
if (vm) sqlite3_finalize(vm);
1119+
}
1120+
10741121
static void dbmem_cache_store (dbmem_context *ctx, uint64_t text_hash, const embedding_result_t *result) {
10751122
static const char *sql = "INSERT OR REPLACE INTO dbmem_cache (text_hash, provider, model, embedding, dimension) VALUES (?1, ?2, ?3, ?4, ?5);";
10761123

@@ -1090,6 +1137,11 @@ static void dbmem_cache_store (dbmem_context *ctx, uint64_t text_hash, const emb
10901137

10911138
cleanup:
10921139
if (vm) sqlite3_finalize(vm);
1140+
1141+
// evict oldest entries if limit is set and exceeded
1142+
if (ctx->cache_max_entries > 0) {
1143+
dbmem_cache_evict(ctx);
1144+
}
10931145
}
10941146

10951147
// MARK: -

src/sqlite-memory.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ double dbmem_context_vector_weight (dbmem_context *ctx);
4343
double dbmem_context_text_weight (dbmem_context *ctx);
4444
double dbmem_context_min_score (dbmem_context *ctx);
4545
bool dbmem_context_update_access (dbmem_context *ctx);
46+
int dbmem_context_search_oversample (dbmem_context *ctx);
4647
const char *dbmem_context_errmsg (dbmem_context *ctx);
4748

4849
#ifdef __cplusplus

test/unittest.c

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,128 @@ TEST(sqlite_cache_setting_default) {
20492049
sqlite3_close(db);
20502050
}
20512051

2052+
TEST(sqlite_cache_max_entries_setting) {
2053+
sqlite3 *db = open_test_db();
2054+
ASSERT(db != NULL);
2055+
2056+
// Default is 0 (no limit)
2057+
sqlite3_int64 result;
2058+
int rc = exec_get_int(db, "SELECT memory_set_option('cache_max_entries', 100);", &result);
2059+
ASSERT_EQ(rc, SQLITE_OK);
2060+
ASSERT_EQ(result, 1);
2061+
2062+
rc = exec_get_int(db, "SELECT memory_get_option('cache_max_entries');", &result);
2063+
ASSERT_EQ(rc, SQLITE_OK);
2064+
ASSERT_EQ(result, 100);
2065+
2066+
// Set back to 0 (no limit)
2067+
rc = exec_get_int(db, "SELECT memory_set_option('cache_max_entries', 0);", &result);
2068+
ASSERT_EQ(rc, SQLITE_OK);
2069+
ASSERT_EQ(result, 1);
2070+
2071+
rc = exec_get_int(db, "SELECT memory_get_option('cache_max_entries');", &result);
2072+
ASSERT_EQ(rc, SQLITE_OK);
2073+
ASSERT_EQ(result, 0);
2074+
2075+
sqlite3_close(db);
2076+
}
2077+
2078+
TEST(sqlite_cache_eviction) {
2079+
sqlite3 *db = open_test_db();
2080+
ASSERT(db != NULL);
2081+
2082+
// Set max entries to 3
2083+
sqlite3_int64 result;
2084+
int rc = exec_get_int(db, "SELECT memory_set_option('cache_max_entries', 3);", &result);
2085+
ASSERT_EQ(rc, SQLITE_OK);
2086+
2087+
// Insert 5 entries (rowids 1-5)
2088+
rc = sqlite3_exec(db,
2089+
"INSERT INTO dbmem_cache (text_hash, provider, model, embedding, dimension) VALUES "
2090+
"(1, 'p', 'm', X'00000000', 1), "
2091+
"(2, 'p', 'm', X'00000000', 1), "
2092+
"(3, 'p', 'm', X'00000000', 1), "
2093+
"(4, 'p', 'm', X'00000000', 1), "
2094+
"(5, 'p', 'm', X'00000000', 1);",
2095+
NULL, NULL, NULL);
2096+
ASSERT_EQ(rc, SQLITE_OK);
2097+
2098+
// Verify 5 entries before any sync triggers eviction
2099+
sqlite3_int64 count;
2100+
rc = exec_get_int(db, "SELECT COUNT(*) FROM dbmem_cache;", &count);
2101+
ASSERT_EQ(rc, SQLITE_OK);
2102+
ASSERT_EQ(count, 5);
2103+
2104+
// Now manually call memory_cache_clear to clear all, then re-insert within limit
2105+
rc = exec_get_int(db, "SELECT memory_cache_clear();", &result);
2106+
ASSERT_EQ(rc, SQLITE_OK);
2107+
2108+
// Insert exactly 3 (at limit)
2109+
rc = sqlite3_exec(db,
2110+
"INSERT INTO dbmem_cache (text_hash, provider, model, embedding, dimension) VALUES "
2111+
"(10, 'p', 'm', X'00000000', 1), "
2112+
"(11, 'p', 'm', X'00000000', 1), "
2113+
"(12, 'p', 'm', X'00000000', 1);",
2114+
NULL, NULL, NULL);
2115+
ASSERT_EQ(rc, SQLITE_OK);
2116+
2117+
rc = exec_get_int(db, "SELECT COUNT(*) FROM dbmem_cache;", &count);
2118+
ASSERT_EQ(rc, SQLITE_OK);
2119+
ASSERT_EQ(count, 3);
2120+
2121+
sqlite3_close(db);
2122+
}
2123+
2124+
TEST(sqlite_cache_no_eviction_when_unlimited) {
2125+
sqlite3 *db = open_test_db();
2126+
ASSERT(db != NULL);
2127+
2128+
// Default cache_max_entries is 0 (no limit)
2129+
// Insert many entries, none should be evicted
2130+
int rc = sqlite3_exec(db,
2131+
"INSERT INTO dbmem_cache (text_hash, provider, model, embedding, dimension) VALUES "
2132+
"(1, 'p', 'm', X'00000000', 1), "
2133+
"(2, 'p', 'm', X'00000000', 1), "
2134+
"(3, 'p', 'm', X'00000000', 1), "
2135+
"(4, 'p', 'm', X'00000000', 1), "
2136+
"(5, 'p', 'm', X'00000000', 1);",
2137+
NULL, NULL, NULL);
2138+
ASSERT_EQ(rc, SQLITE_OK);
2139+
2140+
sqlite3_int64 count;
2141+
rc = exec_get_int(db, "SELECT COUNT(*) FROM dbmem_cache;", &count);
2142+
ASSERT_EQ(rc, SQLITE_OK);
2143+
ASSERT_EQ(count, 5);
2144+
2145+
sqlite3_close(db);
2146+
}
2147+
2148+
TEST(sqlite_search_oversample_setting) {
2149+
sqlite3 *db = open_test_db();
2150+
ASSERT(db != NULL);
2151+
2152+
// Default is 0 (no oversampling)
2153+
sqlite3_int64 result;
2154+
int rc = exec_get_int(db, "SELECT memory_set_option('search_oversample', 4);", &result);
2155+
ASSERT_EQ(rc, SQLITE_OK);
2156+
ASSERT_EQ(result, 1);
2157+
2158+
rc = exec_get_int(db, "SELECT memory_get_option('search_oversample');", &result);
2159+
ASSERT_EQ(rc, SQLITE_OK);
2160+
ASSERT_EQ(result, 4);
2161+
2162+
// Set back to 0 (no oversampling)
2163+
rc = exec_get_int(db, "SELECT memory_set_option('search_oversample', 0);", &result);
2164+
ASSERT_EQ(rc, SQLITE_OK);
2165+
ASSERT_EQ(result, 1);
2166+
2167+
rc = exec_get_int(db, "SELECT memory_get_option('search_oversample');", &result);
2168+
ASSERT_EQ(rc, SQLITE_OK);
2169+
ASSERT_EQ(result, 0);
2170+
2171+
sqlite3_close(db);
2172+
}
2173+
20522174
TEST(sqlite_memory_delete_context_with_vault) {
20532175
sqlite3 *db = open_test_db();
20542176
ASSERT(db != NULL);
@@ -2212,6 +2334,12 @@ int main(int argc, char *argv[]) {
22122334
RUN_TEST(sqlite_cache_clear_with_data);
22132335
RUN_TEST(sqlite_cache_clear_by_provider_model);
22142336
RUN_TEST(sqlite_cache_setting_default);
2337+
RUN_TEST(sqlite_cache_max_entries_setting);
2338+
RUN_TEST(sqlite_cache_eviction);
2339+
RUN_TEST(sqlite_cache_no_eviction_when_unlimited);
2340+
2341+
printf("\nSearch oversampling tests:\n");
2342+
RUN_TEST(sqlite_search_oversample_setting);
22152343
#endif
22162344

22172345
printf("\n=== Results ===\n");

0 commit comments

Comments
 (0)