Merge pull request #570 from ggozad/refactor/doctor-checks
Make run_db_checks an orchestration list
This commit is contained in:
commit
d6cfda22b5
1 changed files with 238 additions and 216 deletions
|
|
@ -433,6 +433,227 @@ def _check_duplicate_documents(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_document_meta_parity(
|
||||||
|
doc_ids: set[str], meta_doc_ids: set[str]
|
||||||
|
) -> CheckResult:
|
||||||
|
"""documents <-> document_meta must be 1:1."""
|
||||||
|
orphan_docs = doc_ids - meta_doc_ids
|
||||||
|
orphan_meta = meta_doc_ids - doc_ids
|
||||||
|
if not (orphan_docs or orphan_meta):
|
||||||
|
return CheckResult(
|
||||||
|
name="document_meta_parity",
|
||||||
|
severity=Severity.OK,
|
||||||
|
message="documents and document_meta are consistent.",
|
||||||
|
)
|
||||||
|
details = [f"document with no meta: {d}" for d in _sample(sorted(orphan_docs))]
|
||||||
|
details += [f"meta with no document: {d}" for d in _sample(sorted(orphan_meta))]
|
||||||
|
return CheckResult(
|
||||||
|
name="document_meta_parity",
|
||||||
|
severity=Severity.FAIL,
|
||||||
|
message="documents and document_meta are out of sync.",
|
||||||
|
remediation="haiku-rag rebuild",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_orphaned_chunks(chunk_doc_ids: set[str], doc_ids: set[str]) -> CheckResult:
|
||||||
|
"""Chunks referencing a document that no longer exists."""
|
||||||
|
orphans = chunk_doc_ids - doc_ids
|
||||||
|
return CheckResult(
|
||||||
|
name="orphaned_chunks",
|
||||||
|
severity=Severity.FAIL if orphans else Severity.OK,
|
||||||
|
message=(
|
||||||
|
"Chunks reference missing documents." if orphans else "No orphaned chunks."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild" if orphans else None,
|
||||||
|
details=_sample(sorted(orphans)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_orphaned_items(item_doc_ids: set[str], doc_ids: set[str]) -> CheckResult:
|
||||||
|
"""Document items referencing a document that no longer exists."""
|
||||||
|
orphans = item_doc_ids - doc_ids
|
||||||
|
return CheckResult(
|
||||||
|
name="orphaned_document_items",
|
||||||
|
severity=Severity.FAIL if orphans else Severity.OK,
|
||||||
|
message=(
|
||||||
|
"Document items reference missing documents."
|
||||||
|
if orphans
|
||||||
|
else "No orphaned document items."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild" if orphans else None,
|
||||||
|
details=_sample(sorted(orphans)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_documents_without_items(
|
||||||
|
doc_ids: set[str], chunk_doc_ids: set[str], item_doc_ids: set[str]
|
||||||
|
) -> CheckResult:
|
||||||
|
"""A chunked document must have items; one without them is corrupt. Empty
|
||||||
|
documents legitimately have neither, so only chunked ones are flagged."""
|
||||||
|
missing = (doc_ids & chunk_doc_ids) - item_doc_ids
|
||||||
|
return CheckResult(
|
||||||
|
name="documents_without_items",
|
||||||
|
severity=Severity.WARN if missing else Severity.OK,
|
||||||
|
message=(
|
||||||
|
f"{len(missing)} chunked document(s) have no document items."
|
||||||
|
if missing
|
||||||
|
else "Every chunked document has document items."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild" if missing else None,
|
||||||
|
details=_sample(sorted(missing)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_dangling_item_refs(
|
||||||
|
chunk_rows: list[dict], self_refs_by_doc: dict[str, set[str]]
|
||||||
|
) -> CheckResult:
|
||||||
|
"""Chunk metadata may reference self_refs that do not exist for that document."""
|
||||||
|
dangling: list[str] = []
|
||||||
|
for row in chunk_rows:
|
||||||
|
refs = json.loads(row.get("metadata") or "{}").get("doc_item_refs") or []
|
||||||
|
known = self_refs_by_doc.get(row["document_id"], set())
|
||||||
|
if any(ref not in known for ref in refs):
|
||||||
|
dangling.append(row["id"])
|
||||||
|
return CheckResult(
|
||||||
|
name="dangling_doc_item_refs",
|
||||||
|
severity=Severity.FAIL if dangling else Severity.OK,
|
||||||
|
message=(
|
||||||
|
f"{len(dangling)} chunk(s) reference missing document items."
|
||||||
|
if dangling
|
||||||
|
else "All chunk doc_item_refs resolve."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild" if dangling else None,
|
||||||
|
details=_sample(dangling),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_vector_dimension(stored_dim: int | None, actual_dim: int) -> CheckResult:
|
||||||
|
if stored_dim and stored_dim != actual_dim:
|
||||||
|
return CheckResult(
|
||||||
|
name="vector_dimension",
|
||||||
|
severity=Severity.FAIL,
|
||||||
|
message=(
|
||||||
|
f"Chunk vector size {actual_dim} does not match stored "
|
||||||
|
f"vector_dim {stored_dim}."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild",
|
||||||
|
)
|
||||||
|
return CheckResult(
|
||||||
|
name="vector_dimension",
|
||||||
|
severity=Severity.OK,
|
||||||
|
message=f"Chunk vectors are {actual_dim}-dimensional.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_unembedded_chunks(id_column, embedded: "np.ndarray") -> CheckResult:
|
||||||
|
"""All-zero vectors, reported as a count with a few sampled ids so a large
|
||||||
|
corpus never materializes every chunk id."""
|
||||||
|
zero_rows = np.nonzero(~embedded)[0]
|
||||||
|
zero_count = int(zero_rows.size)
|
||||||
|
sample = [id_column[int(i)].as_py() for i in zero_rows[:_SAMPLE_LIMIT]]
|
||||||
|
if zero_count > _SAMPLE_LIMIT:
|
||||||
|
sample.append(f"... (+{zero_count - _SAMPLE_LIMIT} more)")
|
||||||
|
return CheckResult(
|
||||||
|
name="unembedded_chunks",
|
||||||
|
severity=Severity.WARN if zero_count else Severity.OK,
|
||||||
|
message=(
|
||||||
|
f"{zero_count} chunk(s) have an all-zero (unembedded) vector."
|
||||||
|
if zero_count
|
||||||
|
else "All chunks are embedded."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild --embed-only" if zero_count else None,
|
||||||
|
details=sample,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _document_centroids(
|
||||||
|
document_id_column, vectors: "np.ndarray", embedded: "np.ndarray", dim: int
|
||||||
|
) -> tuple[list[str], "np.ndarray", "np.ndarray"]:
|
||||||
|
"""Reduce each document's chunk vectors to one summed centroid.
|
||||||
|
|
||||||
|
Dictionary-encode the document ids into integer codes, then sum each
|
||||||
|
document's embedded rows in a single pass per document — no second full copy
|
||||||
|
of the vector matrix. Returns (document ids, summed centroids, chunk counts);
|
||||||
|
the caller normalizes.
|
||||||
|
"""
|
||||||
|
encoded = document_id_column.combine_chunks().dictionary_encode()
|
||||||
|
ids = encoded.dictionary.to_pylist()
|
||||||
|
codes = encoded.indices.to_numpy(zero_copy_only=False)
|
||||||
|
centroids = np.zeros((len(ids), dim), dtype=np.float32)
|
||||||
|
counts = np.zeros(len(ids), dtype=np.int64)
|
||||||
|
order = np.argsort(codes, kind="stable")
|
||||||
|
bounds = np.searchsorted(codes, np.arange(len(ids) + 1), sorter=order)
|
||||||
|
for d in range(len(ids)):
|
||||||
|
rows = order[bounds[d] : bounds[d + 1]]
|
||||||
|
rows = rows[embedded[rows]]
|
||||||
|
counts[d] = rows.size
|
||||||
|
if rows.size:
|
||||||
|
centroids[d] = vectors[rows].sum(axis=0)
|
||||||
|
return ids, centroids, counts
|
||||||
|
|
||||||
|
|
||||||
|
def _check_picture_data(
|
||||||
|
missing_picture_docs: list[str], content_type_by_doc: dict[str, str]
|
||||||
|
) -> CheckResult:
|
||||||
|
"""Pictures from image/PDF sources should carry raster bytes. Pictures that
|
||||||
|
are external image references in a text document (markdown, HTML) have no
|
||||||
|
embedded bytes by nature, so a missing raster there is expected."""
|
||||||
|
real_missing = [
|
||||||
|
doc_id
|
||||||
|
for doc_id in missing_picture_docs
|
||||||
|
if not content_type_by_doc.get(doc_id, "").startswith("text/")
|
||||||
|
]
|
||||||
|
return CheckResult(
|
||||||
|
name="picture_data",
|
||||||
|
severity=Severity.WARN if real_missing else Severity.OK,
|
||||||
|
message=(
|
||||||
|
f"{len(real_missing)} picture item(s) in image/PDF documents "
|
||||||
|
"have no image data."
|
||||||
|
if real_missing
|
||||||
|
else "Pictures that should carry image data have it."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag rebuild" if real_missing else None,
|
||||||
|
details=_sample(sorted(set(real_missing))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_settings_row(total_settings: int, canonical: int) -> CheckResult:
|
||||||
|
"""Settings must hold exactly one canonical row."""
|
||||||
|
if total_settings == 0 or canonical != 1:
|
||||||
|
return CheckResult(
|
||||||
|
name="settings_row",
|
||||||
|
severity=Severity.FAIL,
|
||||||
|
message=(
|
||||||
|
f"Expected exactly one 'settings' row, found {canonical} "
|
||||||
|
f"(of {total_settings} total)."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag migrate",
|
||||||
|
)
|
||||||
|
return CheckResult(
|
||||||
|
name="settings_row",
|
||||||
|
severity=Severity.OK,
|
||||||
|
message="Settings row is present.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_pending_migrations(stored_version: str) -> CheckResult:
|
||||||
|
pending = (
|
||||||
|
get_pending_upgrades(stored_version) if stored_version != "unknown" else []
|
||||||
|
)
|
||||||
|
return CheckResult(
|
||||||
|
name="pending_migrations",
|
||||||
|
severity=Severity.WARN if pending else Severity.OK,
|
||||||
|
message=(
|
||||||
|
f"{len(pending)} migration(s) pending (db version {stored_version})."
|
||||||
|
if pending
|
||||||
|
else f"Database is up to date (version {stored_version})."
|
||||||
|
),
|
||||||
|
remediation="haiku-rag migrate" if pending else None,
|
||||||
|
details=[f"{step.version}: {step.description or ''}" for step in pending],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_db_checks(
|
async def run_db_checks(
|
||||||
store: Store,
|
store: Store,
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
|
|
@ -484,110 +705,22 @@ async def run_db_checks(
|
||||||
labels_by_doc.setdefault(row["document_id"], set()).add(row["label"])
|
labels_by_doc.setdefault(row["document_id"], set()).add(row["label"])
|
||||||
|
|
||||||
notify("Checking referential integrity")
|
notify("Checking referential integrity")
|
||||||
# documents <-> document_meta must be 1:1.
|
results.append(_check_document_meta_parity(doc_ids, meta_doc_ids))
|
||||||
orphan_docs = doc_ids - meta_doc_ids
|
results.append(_check_orphaned_chunks(chunk_doc_ids, doc_ids))
|
||||||
orphan_meta = meta_doc_ids - doc_ids
|
results.append(_check_orphaned_items(item_doc_ids, doc_ids))
|
||||||
if orphan_docs or orphan_meta:
|
|
||||||
details = [f"document with no meta: {d}" for d in _sample(sorted(orphan_docs))]
|
|
||||||
details += [f"meta with no document: {d}" for d in _sample(sorted(orphan_meta))]
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="document_meta_parity",
|
|
||||||
severity=Severity.FAIL,
|
|
||||||
message="documents and document_meta are out of sync.",
|
|
||||||
remediation="haiku-rag rebuild",
|
|
||||||
details=details,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="document_meta_parity",
|
|
||||||
severity=Severity.OK,
|
|
||||||
message="documents and document_meta are consistent.",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Orphaned chunks / items reference a document that no longer exists.
|
|
||||||
orphan_chunk_docs = chunk_doc_ids - doc_ids
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="orphaned_chunks",
|
|
||||||
severity=Severity.FAIL if orphan_chunk_docs else Severity.OK,
|
|
||||||
message=(
|
|
||||||
"Chunks reference missing documents."
|
|
||||||
if orphan_chunk_docs
|
|
||||||
else "No orphaned chunks."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild" if orphan_chunk_docs else None,
|
|
||||||
details=_sample(sorted(orphan_chunk_docs)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
orphan_item_docs = item_doc_ids - doc_ids
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="orphaned_document_items",
|
|
||||||
severity=Severity.FAIL if orphan_item_docs else Severity.OK,
|
|
||||||
message=(
|
|
||||||
"Document items reference missing documents."
|
|
||||||
if orphan_item_docs
|
|
||||||
else "No orphaned document items."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild" if orphan_item_docs else None,
|
|
||||||
details=_sample(sorted(orphan_item_docs)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
notify("Checking document chunking")
|
notify("Checking document chunking")
|
||||||
# Documents with no chunks, classified by what they contain and whether the
|
|
||||||
# embedder can index images.
|
|
||||||
results += _classify_unchunked(
|
results += _classify_unchunked(
|
||||||
doc_ids - chunk_doc_ids, labels_by_doc, store.embedder.supports_images
|
doc_ids - chunk_doc_ids, labels_by_doc, store.embedder.supports_images
|
||||||
)
|
)
|
||||||
|
results.append(_check_documents_without_items(doc_ids, chunk_doc_ids, item_doc_ids))
|
||||||
# A chunked document must have items; one without them is corrupt. Empty
|
|
||||||
# documents legitimately have neither, so only flag the chunked ones.
|
|
||||||
docs_missing_items = (doc_ids & chunk_doc_ids) - item_doc_ids
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="documents_without_items",
|
|
||||||
severity=Severity.WARN if docs_missing_items else Severity.OK,
|
|
||||||
message=(
|
|
||||||
f"{len(docs_missing_items)} chunked document(s) have no document items."
|
|
||||||
if docs_missing_items
|
|
||||||
else "Every chunked document has document items."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild" if docs_missing_items else None,
|
|
||||||
details=_sample(sorted(docs_missing_items)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
notify("Checking chunk references")
|
notify("Checking chunk references")
|
||||||
# Chunk metadata may reference self_refs that do not exist for that document.
|
results.append(_check_dangling_item_refs(chunk_rows, self_refs_by_doc))
|
||||||
dangling: list[str] = []
|
|
||||||
for row in chunk_rows:
|
|
||||||
refs = json.loads(row.get("metadata") or "{}").get("doc_item_refs") or []
|
|
||||||
known = self_refs_by_doc.get(row["document_id"], set())
|
|
||||||
if any(ref not in known for ref in refs):
|
|
||||||
dangling.append(row["id"])
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="dangling_doc_item_refs",
|
|
||||||
severity=Severity.FAIL if dangling else Severity.OK,
|
|
||||||
message=(
|
|
||||||
f"{len(dangling)} chunk(s) reference missing document items."
|
|
||||||
if dangling
|
|
||||||
else "All chunk doc_item_refs resolve."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild" if dangling else None,
|
|
||||||
details=_sample(dangling),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
notify("Scanning chunk vectors")
|
notify("Scanning chunk vectors")
|
||||||
# Vector dimension consistency and unembedded (all-zero) vectors share one
|
# Vector dimension, unembedded vectors and duplicate detection share one
|
||||||
# scan of the vector column — the heaviest check on large corpora.
|
# scan of the vector column — the heaviest read on large corpora.
|
||||||
arrow = (
|
arrow = (
|
||||||
await store.chunks_table.query()
|
await store.chunks_table.query()
|
||||||
.select(["id", "vector", "document_id"])
|
.select(["id", "vector", "document_id"])
|
||||||
|
|
@ -596,26 +729,7 @@ async def run_db_checks(
|
||||||
stored = await SettingsRepository(store).get_current_settings()
|
stored = await SettingsRepository(store).get_current_settings()
|
||||||
stored_dim = stored.get("embeddings", {}).get("model", {}).get("vector_dim")
|
stored_dim = stored.get("embeddings", {}).get("model", {}).get("vector_dim")
|
||||||
actual_dim = arrow.schema.field("vector").type.list_size
|
actual_dim = arrow.schema.field("vector").type.list_size
|
||||||
if stored_dim and stored_dim != actual_dim:
|
results.append(_check_vector_dimension(stored_dim, actual_dim))
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="vector_dimension",
|
|
||||||
severity=Severity.FAIL,
|
|
||||||
message=(
|
|
||||||
f"Chunk vector size {actual_dim} does not match stored "
|
|
||||||
f"vector_dim {stored_dim}."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="vector_dimension",
|
|
||||||
severity=Severity.OK,
|
|
||||||
message=f"Chunk vectors are {actual_dim}-dimensional.",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reshape the Arrow fixed-size-list child buffer directly into an (N, dim)
|
# Reshape the Arrow fixed-size-list child buffer directly into an (N, dim)
|
||||||
# float32 matrix. Going through to_pylist() would box N*dim Python floats
|
# float32 matrix. Going through to_pylist() would box N*dim Python floats
|
||||||
|
|
@ -625,50 +739,17 @@ async def run_db_checks(
|
||||||
vectors = vec_col.values.to_numpy(zero_copy_only=False).reshape(-1, actual_dim)
|
vectors = vec_col.values.to_numpy(zero_copy_only=False).reshape(-1, actual_dim)
|
||||||
embedded = vectors.any(axis=1) if vectors.size else np.zeros(0, dtype=bool)
|
embedded = vectors.any(axis=1) if vectors.size else np.zeros(0, dtype=bool)
|
||||||
|
|
||||||
# Unembedded (all-zero) chunks: report a count and a few sampled ids without
|
results.append(_check_unembedded_chunks(arrow.column("id"), embedded))
|
||||||
# materializing every chunk id.
|
|
||||||
zero_rows = np.nonzero(~embedded)[0]
|
|
||||||
zero_count = int(zero_rows.size)
|
|
||||||
id_col = arrow.column("id")
|
|
||||||
zero_sample = [id_col[int(i)].as_py() for i in zero_rows[:_SAMPLE_LIMIT]]
|
|
||||||
if zero_count > _SAMPLE_LIMIT:
|
|
||||||
zero_sample.append(f"... (+{zero_count - _SAMPLE_LIMIT} more)")
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="unembedded_chunks",
|
|
||||||
severity=Severity.WARN if zero_count else Severity.OK,
|
|
||||||
message=(
|
|
||||||
f"{zero_count} chunk(s) have an all-zero (unembedded) vector."
|
|
||||||
if zero_count
|
|
||||||
else "All chunks are embedded."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild --embed-only" if zero_count else None,
|
|
||||||
details=zero_sample,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
notify("Detecting near-duplicate documents")
|
notify("Detecting near-duplicate documents")
|
||||||
# Near-identical documents (centroid cosine). Reduce each document's chunk
|
centroid_doc_ids, centroids, counts = _document_centroids(
|
||||||
# vectors to one summed centroid during the scan: dictionary-encode the
|
arrow.column("document_id"), vectors, embedded, actual_dim
|
||||||
# document ids into integer codes, then sum each document's embedded rows in
|
)
|
||||||
# a single pass per document — no second full copy of the vector matrix.
|
# The matrix is the largest object here; drop it before clustering.
|
||||||
encoded = arrow.column("document_id").combine_chunks().dictionary_encode()
|
|
||||||
doc_ids = encoded.dictionary.to_pylist()
|
|
||||||
codes = encoded.indices.to_numpy(zero_copy_only=False)
|
|
||||||
centroids = np.zeros((len(doc_ids), actual_dim), dtype=np.float32)
|
|
||||||
counts = np.zeros(len(doc_ids), dtype=np.int64)
|
|
||||||
order = np.argsort(codes, kind="stable")
|
|
||||||
bounds = np.searchsorted(codes, np.arange(len(doc_ids) + 1), sorter=order)
|
|
||||||
for d in range(len(doc_ids)):
|
|
||||||
rows = order[bounds[d] : bounds[d + 1]]
|
|
||||||
rows = rows[embedded[rows]]
|
|
||||||
counts[d] = rows.size
|
|
||||||
if rows.size:
|
|
||||||
centroids[d] = vectors[rows].sum(axis=0)
|
|
||||||
del vectors
|
del vectors
|
||||||
results.append(
|
results.append(
|
||||||
_check_duplicate_documents(
|
_check_duplicate_documents(
|
||||||
doc_ids,
|
centroid_doc_ids,
|
||||||
centroids,
|
centroids,
|
||||||
counts,
|
counts,
|
||||||
uri_by_doc,
|
uri_by_doc,
|
||||||
|
|
@ -679,9 +760,6 @@ async def run_db_checks(
|
||||||
)
|
)
|
||||||
|
|
||||||
notify("Checking picture data")
|
notify("Checking picture data")
|
||||||
# Pictures from image/PDF sources should carry raster bytes. Pictures that
|
|
||||||
# are external image references in a text document (markdown, HTML) have no
|
|
||||||
# embedded bytes by nature, so a missing raster there is expected.
|
|
||||||
missing_picture_docs = [
|
missing_picture_docs = [
|
||||||
row["document_id"]
|
row["document_id"]
|
||||||
for row in await store.document_items_table.query()
|
for row in await store.document_items_table.query()
|
||||||
|
|
@ -689,72 +767,16 @@ async def run_db_checks(
|
||||||
.where("label = 'picture' AND picture_data IS NULL")
|
.where("label = 'picture' AND picture_data IS NULL")
|
||||||
.to_list()
|
.to_list()
|
||||||
]
|
]
|
||||||
real_missing = [
|
results.append(_check_picture_data(missing_picture_docs, content_type_by_doc))
|
||||||
doc_id
|
|
||||||
for doc_id in missing_picture_docs
|
|
||||||
if not content_type_by_doc.get(doc_id, "").startswith("text/")
|
|
||||||
]
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="picture_data",
|
|
||||||
severity=Severity.WARN if real_missing else Severity.OK,
|
|
||||||
message=(
|
|
||||||
f"{len(real_missing)} picture item(s) in image/PDF documents "
|
|
||||||
"have no image data."
|
|
||||||
if real_missing
|
|
||||||
else "Pictures that should carry image data have it."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag rebuild" if real_missing else None,
|
|
||||||
details=_sample(sorted(set(real_missing))),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
notify("Checking settings and indexes")
|
notify("Checking settings and indexes")
|
||||||
# Settings must hold exactly one canonical row.
|
|
||||||
total_settings = await store.settings_table.count_rows()
|
total_settings = await store.settings_table.count_rows()
|
||||||
canonical = len(
|
canonical = len(
|
||||||
await store.settings_table.query().where("id = 'settings'").to_list()
|
await store.settings_table.query().where("id = 'settings'").to_list()
|
||||||
)
|
)
|
||||||
if total_settings == 0 or canonical != 1:
|
results.append(_check_settings_row(total_settings, canonical))
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="settings_row",
|
|
||||||
severity=Severity.FAIL,
|
|
||||||
message=(
|
|
||||||
f"Expected exactly one 'settings' row, found {canonical} "
|
|
||||||
f"(of {total_settings} total)."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag migrate",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="settings_row",
|
|
||||||
severity=Severity.OK,
|
|
||||||
message="Settings row is present.",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
results.append(_check_embedding_drift(stored, config))
|
results.append(_check_embedding_drift(stored, config))
|
||||||
|
results.append(_check_pending_migrations(str(stored.get("version", "unknown"))))
|
||||||
stored_version = str(stored.get("version", "unknown"))
|
|
||||||
pending = (
|
|
||||||
get_pending_upgrades(stored_version) if stored_version != "unknown" else []
|
|
||||||
)
|
|
||||||
results.append(
|
|
||||||
CheckResult(
|
|
||||||
name="pending_migrations",
|
|
||||||
severity=Severity.WARN if pending else Severity.OK,
|
|
||||||
message=(
|
|
||||||
f"{len(pending)} migration(s) pending (db version {stored_version})."
|
|
||||||
if pending
|
|
||||||
else f"Database is up to date (version {stored_version})."
|
|
||||||
),
|
|
||||||
remediation="haiku-rag migrate" if pending else None,
|
|
||||||
details=[f"{step.version}: {step.description or ''}" for step in pending],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
results.append(_check_vector_index(stats))
|
results.append(_check_vector_index(stats))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue