Skip to content

Commit 5816996

Browse files
committed
Enhance ChromaDB recovery with brute-force fallback
The standard data extraction for ChromaDB recovery relies on correctly scoped 'VECTOR' segments. This commit introduces a brute-force extraction method that ignores segment scope, allowing recovery of data even if the 'VECTOR' segment is corrupted or missing. The `recover_collection` function now attempts standard extraction first and falls back to brute force if no records are found, significantly increasing the robustness of the recovery process against database corruption.
1 parent 84e2029 commit 5816996

1 file changed

Lines changed: 44 additions & 18 deletions

File tree

src/agentforge/storage/chroma_recover.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,24 +100,37 @@ def backup_target_files(db_path: str, segment_uuid: Optional[str]) -> str:
100100
raise RuntimeError("Aborting recovery: Backup failed.")
101101

102102

103-
def extract_raw_data(db_path: str, collection_uuid: str) -> Dict[str, List[Any]]:
104-
"""Dynamically extracts IDs, Documents, and Metadata using exact schema bindings."""
103+
def extract_raw_data(db_path: str, collection_uuid: str, brute_force: bool = False) -> Dict[str, List[Any]]:
104+
"""
105+
Dynamically extracts IDs, Documents, and Metadata using exact schema bindings.
106+
If brute_force is True, it ignores the VECTOR scope constraint and pulls all orphaned data.
107+
"""
105108
sqlite_path = os.path.join(db_path, "chroma.sqlite3")
106109
conn = sqlite3.connect(sqlite_path)
107110
cursor = conn.cursor()
108111

109112
data = {'ids': [], 'documents': [], 'metadatas': [], 'embeddings': []}
110113

111-
logger.info("Extracting data based on exact DB Schema...")
112-
113-
# 1. Map internal integer IDs to the user's string IDs
114-
# 1.5.8 Fix: Explicitly filter by scope = 'VECTOR' to avoid reading Sparse Vector definitions
115-
cursor.execute("""
116-
SELECT e.id, e.embedding_id
117-
FROM embeddings e
118-
JOIN segments s ON e.segment_id = s.id
119-
WHERE s.collection = ? AND s.scope = 'VECTOR'
120-
""", (collection_uuid,))
114+
if brute_force:
115+
logger.info("Extracting data using BRUTE FORCE (ignoring segment scope)...")
116+
cursor.execute("SELECT id FROM segments WHERE collection = ?", (collection_uuid,))
117+
segment_ids = [r[0] for r in cursor.fetchall()]
118+
119+
if not segment_ids:
120+
conn.close()
121+
return data
122+
123+
placeholders = ",".join(["?"] * len(segment_ids))
124+
cursor.execute(f"SELECT DISTINCT id, embedding_id FROM embeddings WHERE segment_id IN ({placeholders})",
125+
segment_ids)
126+
else:
127+
logger.info("Extracting data based on exact DB Schema (VECTOR scope)...")
128+
cursor.execute("""
129+
SELECT e.id, e.embedding_id
130+
FROM embeddings e
131+
JOIN segments s ON e.segment_id = s.id
132+
WHERE s.collection = ? AND s.scope = 'VECTOR'
133+
""", (collection_uuid,))
121134

122135
id_map = {row[0]: row[1] for row in cursor.fetchall()}
123136
internal_ids = list(id_map.keys())
@@ -178,7 +191,7 @@ def extract_raw_data(db_path: str, collection_uuid: str) -> Dict[str, List[Any]]
178191

179192
meta = meta_dict.get(internal_id, {})
180193
if not meta:
181-
meta = {"source": "auto-recovered"}
194+
meta = {"source": "brute-force-recovered" if brute_force else "auto-recovered"}
182195
data['metadatas'].append(meta)
183196

184197
data['embeddings'].append(None) # Force Local SentenceTransformer to rebuild
@@ -198,18 +211,31 @@ def recover_collection(client: Any, db_path: str, collection_name: str, embeddin
198211
logger.info("Safeguarding data...")
199212
backup_target_files(db_path, seg_uuid)
200213

201-
logger.info("Extracting text and metadata from SQLite...")
202-
raw_data = extract_raw_data(db_path, col_uuid)
214+
# -------------------------------------------------------------
215+
# METHOD 1: Try standard extraction first
216+
# -------------------------------------------------------------
217+
raw_data = extract_raw_data(db_path, col_uuid, brute_force=False)
203218
total_records = len(raw_data.get('ids', []))
204219

220+
# -------------------------------------------------------------
221+
# METHOD 2: Fallback to brute force if VECTOR segment is corrupted
222+
# -------------------------------------------------------------
223+
if total_records == 0:
224+
logger.warning("No records found in standard VECTOR segment. Attempting Brute Force recovery...")
225+
raw_data = extract_raw_data(db_path, col_uuid, brute_force=True)
226+
total_records = len(raw_data.get('ids', []))
227+
205228
if total_records == 0:
206229
logger.warning("No records found to recover in any segment.")
207230
return False
208231

209232
try:
210-
logger.info(f"Deleting corrupted HNSW index...")
233+
logger.info(f"Deleting corrupted collection framework...")
211234
client.delete_collection(name=collection_name)
235+
except Exception as e:
236+
logger.warning(f"Could not cleanly delete collection (may already be missing/corrupt): {e}")
212237

238+
try:
213239
logger.info("Rebuilding collection framework...")
214240
new_col = client.create_collection(
215241
name=collection_name,
@@ -219,7 +245,7 @@ def recover_collection(client: Any, db_path: str, collection_name: str, embeddin
219245
logger.error(f"Failed to reset collection: {e}")
220246
return False
221247

222-
logger.info("Injecting data and regenerating vectors (this may take a moment)...")
248+
logger.info(f"Injecting {total_records} recovered records back into the matrix...")
223249
batch_size = 500
224250
stats = RecoveryStats(total_items=total_records)
225251

@@ -299,4 +325,4 @@ def wrapper(self, *args, **kwargs):
299325
else:
300326
raise e
301327

302-
return wrapper
328+
return wrapper

0 commit comments

Comments
 (0)