docsif 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. docsif-0.1.0.dist-info/METADATA +393 -0
  2. docsif-0.1.0.dist-info/RECORD +86 -0
  3. docsif-0.1.0.dist-info/WHEEL +4 -0
  4. docsif-0.1.0.dist-info/entry_points.txt +3 -0
  5. docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. sif/__init__.py +24 -0
  7. sif/_version.py +3 -0
  8. sif/cli/__init__.py +11 -0
  9. sif/cli/commands/__init__.py +18 -0
  10. sif/cli/commands/bench.py +148 -0
  11. sif/cli/commands/collection.py +360 -0
  12. sif/cli/commands/context.py +154 -0
  13. sif/cli/commands/get.py +200 -0
  14. sif/cli/commands/index.py +294 -0
  15. sif/cli/commands/ls.py +67 -0
  16. sif/cli/commands/mcp.py +93 -0
  17. sif/cli/commands/pull.py +111 -0
  18. sif/cli/commands/search.py +574 -0
  19. sif/cli/config.py +206 -0
  20. sif/cli/formatters.py +269 -0
  21. sif/cli/main.py +205 -0
  22. sif/config/__init__.py +30 -0
  23. sif/config/constants.py +52 -0
  24. sif/config/settings.py +217 -0
  25. sif/core/__init__.py +21 -0
  26. sif/core/collection.py +161 -0
  27. sif/core/context.py +167 -0
  28. sif/core/document.py +125 -0
  29. sif/core/models.py +298 -0
  30. sif/database/__init__.py +23 -0
  31. sif/database/connection.py +139 -0
  32. sif/database/database.py +136 -0
  33. sif/database/migrations.py +187 -0
  34. sif/database/repositories.py +500 -0
  35. sif/database/repository.py +171 -0
  36. sif/database/schema.py +362 -0
  37. sif/embedding/__init__.py +22 -0
  38. sif/embedding/cache.py +196 -0
  39. sif/embedding/embedder.py +328 -0
  40. sif/embedding/factory.py +92 -0
  41. sif/embedding/manager.py +200 -0
  42. sif/embedding/model.py +126 -0
  43. sif/indexing/__init__.py +21 -0
  44. sif/indexing/chunker.py +226 -0
  45. sif/indexing/indexer.py +226 -0
  46. sif/indexing/parser.py +192 -0
  47. sif/indexing/scanner.py +148 -0
  48. sif/indexing/watcher.py +156 -0
  49. sif/mcp/README.md +258 -0
  50. sif/mcp/__init__.py +9 -0
  51. sif/mcp/claude_desktop_config.json +16 -0
  52. sif/mcp/cli.py +92 -0
  53. sif/mcp/protocol.py +336 -0
  54. sif/mcp/server.py +433 -0
  55. sif/mcp/server_http.py +54 -0
  56. sif/mcp/test_mcp.py +108 -0
  57. sif/mcp/tools.py +497 -0
  58. sif/mcp/transport_http.py +424 -0
  59. sif/mcp/transport_stdio.py +292 -0
  60. sif/mcp_server/__init__.py +26 -0
  61. sif/mcp_server/handlers.py +68 -0
  62. sif/mcp_server/server.py +58 -0
  63. sif/mcp_server/tools.py +216 -0
  64. sif/mcp_server/transport.py +221 -0
  65. sif/models/__init__.py +70 -0
  66. sif/models/collection.py +51 -0
  67. sif/models/context.py +45 -0
  68. sif/models/document.py +76 -0
  69. sif/models/download.py +208 -0
  70. sif/models/embedding.py +71 -0
  71. sif/models/search.py +92 -0
  72. sif/search/__init__.py +22 -0
  73. sif/search/benchmark.py +110 -0
  74. sif/search/bm25.py +216 -0
  75. sif/search/expansion.py +157 -0
  76. sif/search/hybrid.py +323 -0
  77. sif/search/rerank.py +390 -0
  78. sif/search/rrf.py +186 -0
  79. sif/search/snippets.py +130 -0
  80. sif/search/strategy.py +97 -0
  81. sif/search/vector.py +177 -0
  82. sif/utils/__init__.py +26 -0
  83. sif/utils/logging.py +143 -0
  84. sif/utils/paths.py +82 -0
  85. sif/utils/progress.py +83 -0
  86. sif/utils/text.py +128 -0
sif/database/schema.py ADDED
@@ -0,0 +1,362 @@
1
+ """Database schema definitions for SIF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sqlite3
7
+
8
+
9
+ class SchemaManager:
10
+ """Manages database schema creation and migrations."""
11
+
12
+ def __init__(self, db: sqlite3.Connection, embedding_dim: int = 384) -> None:
13
+ self.db = db
14
+ self.embedding_dim = embedding_dim
15
+
16
+ def create_all(self) -> None:
17
+ """Create all database tables and indexes."""
18
+ self._create_collections_table()
19
+ self._create_documents_table()
20
+ self._create_document_chunks_table()
21
+ self._migrate_path_contexts()
22
+ self._create_contexts_table()
23
+ self._create_llm_cache_table()
24
+ self._create_fts_tables()
25
+ self._create_vector_tables()
26
+ self._create_indexes()
27
+
28
+ def _add_column_if_missing(self, table: str, column: str, dtype: str) -> None:
29
+ """Add a column to a table if it does not already exist."""
30
+ cursor = self.db.execute(f"PRAGMA table_info({table})")
31
+ columns = {row["name"] for row in cursor.fetchall()}
32
+ if column not in columns:
33
+ self.db.execute(f"ALTER TABLE {table} ADD COLUMN {column} {dtype}")
34
+
35
+ def _create_collections_table(self) -> None:
36
+ """Create collections table."""
37
+ self.db.execute("""
38
+ CREATE TABLE IF NOT EXISTS collections (
39
+ id TEXT PRIMARY KEY,
40
+ name TEXT UNIQUE NOT NULL,
41
+ path TEXT NOT NULL,
42
+ pattern TEXT DEFAULT '**/*.md',
43
+ ignore_patterns TEXT DEFAULT '[]',
44
+ include_by_default INTEGER DEFAULT 1,
45
+ description TEXT,
46
+ created_at TEXT NOT NULL,
47
+ updated_at TEXT NOT NULL,
48
+ last_indexed_at TEXT,
49
+ document_count INTEGER DEFAULT 0,
50
+ chunk_count INTEGER DEFAULT 0
51
+ )
52
+ """)
53
+ self._add_column_if_missing("collections", "pre_update_cmd", "TEXT")
54
+
55
+ def _create_documents_table(self) -> None:
56
+ """Create documents table."""
57
+ self.db.execute("""
58
+ CREATE TABLE IF NOT EXISTS documents (
59
+ id TEXT PRIMARY KEY,
60
+ collection_id TEXT NOT NULL,
61
+ path TEXT NOT NULL,
62
+ filename TEXT NOT NULL,
63
+ title TEXT,
64
+ content TEXT NOT NULL,
65
+ checksum TEXT NOT NULL,
66
+ file_size INTEGER DEFAULT 0,
67
+ mtime REAL NOT NULL,
68
+ created_at TEXT NOT NULL,
69
+ updated_at TEXT NOT NULL,
70
+ metadata TEXT DEFAULT '{}',
71
+ FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
72
+ )
73
+ """)
74
+
75
+ def _create_document_chunks_table(self) -> None:
76
+ """Create document chunks table."""
77
+ self.db.execute("""
78
+ CREATE TABLE IF NOT EXISTS document_chunks (
79
+ id TEXT PRIMARY KEY,
80
+ document_id TEXT NOT NULL,
81
+ sequence INTEGER NOT NULL,
82
+ content TEXT NOT NULL,
83
+ start_pos INTEGER DEFAULT 0,
84
+ end_pos INTEGER DEFAULT 0,
85
+ token_count INTEGER DEFAULT 0,
86
+ embedding_id TEXT,
87
+ created_at TEXT NOT NULL,
88
+ FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
89
+ )
90
+ """)
91
+
92
+ def _create_contexts_table(self) -> None:
93
+ """Create unified contexts table."""
94
+ self.db.execute("""
95
+ CREATE TABLE IF NOT EXISTS contexts (
96
+ id TEXT PRIMARY KEY,
97
+ target_id TEXT NOT NULL,
98
+ context_type TEXT NOT NULL CHECK(context_type IN ('path', 'collection', 'global')),
99
+ content TEXT NOT NULL,
100
+ created_at TEXT NOT NULL,
101
+ updated_at TEXT NOT NULL
102
+ )
103
+ """)
104
+
105
+ def _migrate_path_contexts(self) -> None:
106
+ """Migrate path_contexts -> contexts atomically using SAVEPOINT."""
107
+ cursor = self.db.execute(
108
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='path_contexts'"
109
+ )
110
+ if not cursor.fetchone():
111
+ return
112
+
113
+ self.db.execute("SAVEPOINT sp_migration")
114
+ try:
115
+ self._create_contexts_table()
116
+ self.db.execute("""
117
+ INSERT INTO contexts (id, target_id, context_type, content, created_at, updated_at)
118
+ SELECT id, path, 'path', context, created_at, updated_at
119
+ FROM path_contexts
120
+ """)
121
+ self.db.execute("DROP TABLE path_contexts")
122
+ self.db.execute("""
123
+ CREATE INDEX IF NOT EXISTS idx_contexts_target
124
+ ON contexts(target_id, context_type)
125
+ """)
126
+ self.db.execute("RELEASE SAVEPOINT sp_migration")
127
+ except Exception as e:
128
+ self.db.execute("ROLLBACK TO SAVEPOINT sp_migration")
129
+ import sys # noqa: PLC0415
130
+
131
+ print(f"Migration failed: {e}", file=sys.stderr)
132
+ raise
133
+
134
+ def _create_llm_cache_table(self) -> None:
135
+ """Create LLM cache table."""
136
+ self.db.execute("""
137
+ CREATE TABLE IF NOT EXISTS llm_cache (
138
+ id TEXT PRIMARY KEY,
139
+ model_name TEXT NOT NULL,
140
+ prompt_hash TEXT NOT NULL,
141
+ prompt TEXT NOT NULL,
142
+ response TEXT NOT NULL,
143
+ created_at TEXT NOT NULL,
144
+ expires_at TEXT,
145
+ UNIQUE(model_name, prompt_hash)
146
+ )
147
+ """)
148
+
149
+ def _create_fts_tables(self) -> None:
150
+ """Create FTS5 virtual tables for full-text search with triggers."""
151
+
152
+ def _fts_is_misconfigured(table_name: str, expected_content: str) -> bool:
153
+ """Check if an existing FTS5 table lacks the correct content= setting."""
154
+ cursor = self.db.execute(
155
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,)
156
+ )
157
+ row = cursor.fetchone()
158
+ if not row:
159
+ return False # table does not exist; not misconfigured
160
+ sql = row[0] or ""
161
+ return f"content='{expected_content}'" not in sql
162
+
163
+ needs_rebuild_docs = _fts_is_misconfigured("documents_fts", "documents")
164
+ needs_rebuild_chunks = _fts_is_misconfigured("chunks_fts", "document_chunks")
165
+
166
+ if needs_rebuild_docs:
167
+ self.db.execute("DROP TABLE IF EXISTS documents_fts")
168
+ if needs_rebuild_chunks:
169
+ self.db.execute("DROP TABLE IF EXISTS chunks_fts")
170
+
171
+ # Documents FTS table with external content
172
+ self.db.execute("""
173
+ CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
174
+ content,
175
+ content='documents',
176
+ content_rowid='rowid',
177
+ tokenize='porter'
178
+ )
179
+ """)
180
+
181
+ # Chunks FTS table with external content
182
+ self.db.execute("""
183
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
184
+ content,
185
+ content='document_chunks',
186
+ content_rowid='rowid',
187
+ tokenize='porter'
188
+ )
189
+ """)
190
+
191
+ # Triggers for documents_fts
192
+ self.db.execute("""
193
+ CREATE TRIGGER IF NOT EXISTS documents_fts_insert
194
+ AFTER INSERT ON documents
195
+ BEGIN
196
+ INSERT INTO documents_fts(rowid, content) VALUES (new.rowid, new.content);
197
+ END
198
+ """)
199
+ self.db.execute("""
200
+ CREATE TRIGGER IF NOT EXISTS documents_fts_update
201
+ AFTER UPDATE ON documents
202
+ BEGIN
203
+ UPDATE documents_fts SET content = new.content WHERE rowid = old.rowid;
204
+ END
205
+ """)
206
+ self.db.execute("""
207
+ CREATE TRIGGER IF NOT EXISTS documents_fts_delete
208
+ AFTER DELETE ON documents
209
+ BEGIN
210
+ DELETE FROM documents_fts WHERE rowid = old.rowid;
211
+ END
212
+ """)
213
+
214
+ # Triggers for chunks_fts
215
+ self.db.execute("""
216
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_insert
217
+ AFTER INSERT ON document_chunks
218
+ BEGIN
219
+ INSERT INTO chunks_fts(rowid, content) VALUES (new.rowid, new.content);
220
+ END
221
+ """)
222
+ self.db.execute("""
223
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_update
224
+ AFTER UPDATE ON document_chunks
225
+ BEGIN
226
+ UPDATE chunks_fts SET content = new.content WHERE rowid = old.rowid;
227
+ END
228
+ """)
229
+ self.db.execute("""
230
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_delete
231
+ AFTER DELETE ON document_chunks
232
+ BEGIN
233
+ DELETE FROM chunks_fts WHERE rowid = old.rowid;
234
+ END
235
+ """)
236
+
237
+ # Seed existing data only when tables were rebuilt
238
+ if needs_rebuild_docs:
239
+ self.db.execute("""
240
+ INSERT INTO documents_fts(rowid, content)
241
+ SELECT rowid, content FROM documents
242
+ """)
243
+ if needs_rebuild_chunks:
244
+ self.db.execute("""
245
+ INSERT INTO chunks_fts(rowid, content)
246
+ SELECT rowid, content FROM document_chunks
247
+ """)
248
+
249
+ def _create_vector_tables(self) -> None:
250
+ """Create vector tables using sqlite-vec."""
251
+ try:
252
+ self.db.execute("SELECT vec_version()")
253
+ vec_available = True
254
+ except sqlite3.OperationalError:
255
+ vec_available = False
256
+
257
+ if not vec_available:
258
+ return
259
+
260
+ cursor = self.db.execute(
261
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='document_embeddings'"
262
+ )
263
+ row = cursor.fetchone()
264
+ if row and row[0]:
265
+ match = re.search(r"FLOAT\[(\d+)\]", row[0])
266
+ if match:
267
+ existing_dim = int(match.group(1))
268
+ if existing_dim != self.embedding_dim:
269
+ raise RuntimeError(
270
+ f"Embedding dimension mismatch: database has FLOAT[{existing_dim}], "
271
+ f"but settings require FLOAT[{self.embedding_dim}]. "
272
+ f"Rebuild the index with 'sif cleanup' or delete the database file."
273
+ )
274
+
275
+ self.db.execute(f"""
276
+ CREATE VIRTUAL TABLE IF NOT EXISTS document_embeddings USING vec0(
277
+ embedding_id TEXT PRIMARY KEY,
278
+ document_id TEXT NOT NULL,
279
+ chunk_id TEXT,
280
+ embedding FLOAT[{self.embedding_dim}]
281
+ )
282
+ """)
283
+
284
+ def _create_indexes(self) -> None:
285
+ """Create database indexes."""
286
+ # Documents indexes
287
+ self.db.execute("""
288
+ CREATE INDEX IF NOT EXISTS idx_documents_collection
289
+ ON documents(collection_id)
290
+ """)
291
+ self.db.execute("""
292
+ CREATE INDEX IF NOT EXISTS idx_documents_path
293
+ ON documents(path)
294
+ """)
295
+ self.db.execute("""
296
+ CREATE INDEX IF NOT EXISTS idx_documents_checksum
297
+ ON documents(checksum)
298
+ """)
299
+
300
+ # Chunks indexes
301
+ self.db.execute("""
302
+ CREATE INDEX IF NOT EXISTS idx_chunks_document
303
+ ON document_chunks(document_id)
304
+ """)
305
+
306
+ # Contexts indexes
307
+ self.db.execute("""
308
+ CREATE INDEX IF NOT EXISTS idx_contexts_target
309
+ ON contexts(target_id, context_type)
310
+ """)
311
+
312
+ # LLM cache index
313
+ self.db.execute("""
314
+ CREATE INDEX IF NOT EXISTS idx_llm_cache_lookup
315
+ ON llm_cache(model_name, prompt_hash)
316
+ """)
317
+
318
+ def drop_all(self) -> None:
319
+ """Drop all tables (for testing/reset)."""
320
+ tables = [
321
+ "document_embeddings",
322
+ "chunks_fts",
323
+ "documents_fts",
324
+ "document_chunks",
325
+ "documents",
326
+ "contexts",
327
+ "llm_cache",
328
+ "collections",
329
+ ]
330
+ for table in tables:
331
+ try:
332
+ self.db.execute(f"DROP TABLE IF EXISTS {table}")
333
+ except sqlite3.OperationalError:
334
+ pass
335
+ self.db.commit()
336
+
337
+ def get_stats(self) -> dict:
338
+ """Get database statistics."""
339
+ stats = {}
340
+
341
+ # Collection count
342
+ cursor = self.db.execute("SELECT COUNT(*) FROM collections")
343
+ stats["collections"] = cursor.fetchone()[0]
344
+
345
+ # Document count
346
+ cursor = self.db.execute("SELECT COUNT(*) FROM documents")
347
+ stats["documents"] = cursor.fetchone()[0]
348
+
349
+ # Chunk count
350
+ cursor = self.db.execute("SELECT COUNT(*) FROM document_chunks")
351
+ stats["chunks"] = cursor.fetchone()[0]
352
+
353
+ # Context count
354
+ cursor = self.db.execute("SELECT COUNT(*) FROM contexts")
355
+ stats["contexts"] = cursor.fetchone()[0]
356
+
357
+ # Total size
358
+ cursor = self.db.execute("SELECT SUM(file_size) FROM documents")
359
+ result = cursor.fetchone()[0]
360
+ stats["total_size_bytes"] = result or 0
361
+
362
+ return stats
@@ -0,0 +1,22 @@
1
+ """Embedding generation and management for SIF.
2
+
3
+ This package provides embedding functionality using the Factory pattern:
4
+ - Embedding model management
5
+ - Vector generation for documents
6
+ - Model loading and caching
7
+ - Support for GGUF models via llama-cpp-python
8
+ """
9
+
10
+ from sif.embedding.cache import EmbeddingCache
11
+ from sif.embedding.factory import EmbeddingModelFactory
12
+ from sif.embedding.manager import EmbeddingManager
13
+ from sif.embedding.model import EmbeddingModel, ModelType
14
+
15
+
16
+ __all__ = [
17
+ "EmbeddingCache",
18
+ "EmbeddingManager",
19
+ "EmbeddingModel",
20
+ "EmbeddingModelFactory",
21
+ "ModelType",
22
+ ]
sif/embedding/cache.py ADDED
@@ -0,0 +1,196 @@
1
+ """Embedding cache for storing and retrieving embeddings."""
2
+
3
+ import hashlib
4
+ import json
5
+ import sqlite3
6
+ from pathlib import Path
7
+
8
+ from sif.utils.logging import get_logger
9
+
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class EmbeddingCache:
15
+ """Cache for embeddings to avoid recomputation.
16
+
17
+ Uses SQLite for persistent storage of embeddings keyed by
18
+ content hash. This allows embeddings to be reused across
19
+ application restarts.
20
+ """
21
+
22
+ def __init__(self, cache_dir: Path | str) -> None:
23
+ """Initialize embedding cache.
24
+
25
+ Args:
26
+ cache_dir: Directory for cache storage
27
+ """
28
+ self._cache_dir = Path(cache_dir)
29
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+ self._db_path = self._cache_dir / "embeddings_cache.db"
32
+ self._init_db()
33
+
34
+ def _init_db(self) -> None:
35
+ """Initialize the cache database."""
36
+ with sqlite3.connect(self._db_path) as conn:
37
+ conn.execute("""
38
+ CREATE TABLE IF NOT EXISTS embeddings (
39
+ content_hash TEXT PRIMARY KEY,
40
+ embedding TEXT NOT NULL,
41
+ model_id TEXT NOT NULL,
42
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
43
+ )
44
+ """)
45
+
46
+ # Create index for faster lookups
47
+ conn.execute("""
48
+ CREATE INDEX IF NOT EXISTS idx_model
49
+ ON embeddings(model_id)
50
+ """)
51
+
52
+ conn.commit()
53
+
54
+ def _hash_content(self, content: str) -> str:
55
+ """Create a hash of the content.
56
+
57
+ Args:
58
+ content: Content to hash
59
+
60
+ Returns:
61
+ SHA-256 hash of the content
62
+ """
63
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
64
+
65
+ def get(self, content: str, model_id: str = "default") -> list[float] | None:
66
+ """Get cached embedding for content.
67
+
68
+ Args:
69
+ content: Content to look up
70
+ model_id: Model identifier for cache segmentation
71
+
72
+ Returns:
73
+ Cached embedding or None if not found
74
+ """
75
+ content_hash = self._hash_content(content)
76
+
77
+ try:
78
+ with sqlite3.connect(self._db_path) as conn:
79
+ cursor = conn.execute(
80
+ "SELECT embedding FROM embeddings WHERE content_hash = ? AND model_id = ?",
81
+ (content_hash, model_id),
82
+ )
83
+ row = cursor.fetchone()
84
+
85
+ if row:
86
+ return json.loads(row[0])
87
+ except Exception as e:
88
+ logger.warning(f"Error reading from cache: {e}")
89
+
90
+ return None
91
+
92
+ def set(
93
+ self,
94
+ content: str,
95
+ embedding: list[float],
96
+ model_id: str = "default",
97
+ ) -> None:
98
+ """Store embedding in cache.
99
+
100
+ Args:
101
+ content: Content that was embedded
102
+ embedding: The embedding vector
103
+ model_id: Model identifier for cache segmentation
104
+ """
105
+ content_hash = self._hash_content(content)
106
+ embedding_json = json.dumps(embedding)
107
+
108
+ try:
109
+ with sqlite3.connect(self._db_path) as conn:
110
+ conn.execute(
111
+ """
112
+ INSERT OR REPLACE INTO embeddings
113
+ (content_hash, embedding, model_id)
114
+ VALUES (?, ?, ?)
115
+ """,
116
+ (content_hash, embedding_json, model_id),
117
+ )
118
+ conn.commit()
119
+ except Exception as e:
120
+ logger.warning(f"Error writing to cache: {e}")
121
+
122
+ def get_batch(
123
+ self,
124
+ contents: list[str],
125
+ model_id: str = "default",
126
+ ) -> list[list[float] | None]:
127
+ """Get cached embeddings for multiple contents.
128
+
129
+ Args:
130
+ contents: List of contents to look up
131
+ model_id: Model identifier
132
+
133
+ Returns:
134
+ List of embeddings (None if not in cache)
135
+ """
136
+ return [self.get(c, model_id) for c in contents]
137
+
138
+ def set_batch(
139
+ self,
140
+ contents: list[str],
141
+ embeddings: list[list[float]],
142
+ model_id: str = "default",
143
+ ) -> None:
144
+ """Store multiple embeddings in cache.
145
+
146
+ Args:
147
+ contents: List of contents
148
+ embeddings: List of embeddings
149
+ model_id: Model identifier
150
+ """
151
+ for content, embedding in zip(contents, embeddings):
152
+ self.set(content, embedding, model_id)
153
+
154
+ def clear(self, model_id: str | None = None) -> int:
155
+ """Clear cached embeddings.
156
+
157
+ Args:
158
+ model_id: If specified, only clear for this model
159
+
160
+ Returns:
161
+ Number of entries cleared
162
+ """
163
+ try:
164
+ with sqlite3.connect(self._db_path) as conn:
165
+ if model_id:
166
+ cursor = conn.execute(
167
+ "DELETE FROM embeddings WHERE model_id = ?",
168
+ (model_id,),
169
+ )
170
+ else:
171
+ cursor = conn.execute("DELETE FROM embeddings")
172
+
173
+ conn.commit()
174
+ return cursor.rowcount
175
+ except Exception as e:
176
+ logger.warning(f"Error clearing cache: {e}")
177
+ return 0
178
+
179
+ def get_stats(self) -> dict:
180
+ """Get cache statistics.
181
+
182
+ Returns:
183
+ Dictionary with cache statistics
184
+ """
185
+ try:
186
+ with sqlite3.connect(self._db_path) as conn:
187
+ cursor = conn.execute("SELECT COUNT(*), model_id FROM embeddings GROUP BY model_id")
188
+ rows = cursor.fetchall()
189
+
190
+ return {
191
+ "total_entries": sum(r[0] for r in rows),
192
+ "by_model": {r[1]: r[0] for r in rows},
193
+ }
194
+ except Exception as e:
195
+ logger.warning(f"Error getting cache stats: {e}")
196
+ return {"total_entries": 0, "by_model": {}}