mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
v0.42.66.1 fix: honor pgvector HNSW dimension limits (#3440)
* fix(doctor): honor pgvector HNSW dimension limits * fix(ci): stabilize local Docker verification * chore: bump version and changelog (v0.42.66.1) Co-Authored-By: OpenAI Codex <noreply@openai.com> --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
co-authored by
OpenAI Codex
parent
9690140bf3
commit
7a65f182aa
@@ -2,6 +2,13 @@
|
||||
|
||||
All notable changes to GBrain will be documented in this file.
|
||||
|
||||
## [0.42.66.1] - 2026-07-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- `gbrain doctor` now treats embedding columns wider than pgvector's HNSW limit as healthy exact-scan configurations instead of prescribing an index PostgreSQL cannot build.
|
||||
- Local CI now passes an empty Docker mount list correctly and compiles the embedded-WASM smoke binary from container-local storage on Docker Desktop.
|
||||
|
||||
## [0.42.66.0] - 2026-07-24
|
||||
|
||||
**54 verified fixes from the community backlog: background enrichment stops wasting money on dead pages, autopilot stops killing its own healthy runs, and search respects your settings.**
|
||||
|
||||
@@ -502,3 +502,5 @@ T1.5 wiring is partial in v0.40.7.0. Three follow-ups filed in TODOS.md under
|
||||
union widening (`'person' | 'company'` → `string`), facts/eligibility.ts
|
||||
pack-aware `ELIGIBLE_TYPES` wiring, and 3 doctor checks (schema_pack_coverage,
|
||||
schema_pack_writability, schema_pack_mutation_audit).
|
||||
|
||||
- `src/core/vector-index.ts` + `src/commands/doctor.ts:embedding_column_registry` — shared pgvector HNSW eligibility policy. `hnswIndexExpected(columnType, dims)` derives the answer from the canonical `vector`/`halfvec` dimension caps already used by migration index generation. Doctor reports an HNSW-less active embedding column as a healthy exact-scan configuration when its declared width exceeds the applicable pgvector cap, and only emits the index repair recipe when an index is actually supported. Pinned at both cap boundaries by `test/vector-index-lifecycle.test.ts`.
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@
|
||||
"bun": ">=1.3.10"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "0.42.66.0",
|
||||
"version": "0.42.66.1",
|
||||
"overrides": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
"fast-uri": "^3.1.4",
|
||||
|
||||
@@ -19,13 +19,25 @@ set -euo pipefail
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
OUT_BIN="$(mktemp /tmp/gbrain-wasm-check.XXXXXX)"
|
||||
trap 'rm -f "$OUT_BIN"' EXIT
|
||||
# Build from a container-local copy. On Docker Desktop, Bun canonicalizes a
|
||||
# bind-mounted input to /run/host_virtiofs but keeps /app as the output path;
|
||||
# its final atomic rename then fails with ENOENT even though both names refer
|
||||
# to the same mount. Keeping inputs and output under /tmp avoids that alias.
|
||||
BUILD_DIR="$(mktemp -d /tmp/gbrain-wasm-check.XXXXXX)"
|
||||
OUT_BIN="$BUILD_DIR/chunker-smoketest"
|
||||
trap 'rm -rf "$BUILD_DIR"' EXIT
|
||||
mkdir -p "$BUILD_DIR/scripts"
|
||||
cp -R "$REPO_ROOT/src" "$BUILD_DIR/src"
|
||||
cp "$REPO_ROOT/scripts/chunker-smoketest.ts" "$BUILD_DIR/scripts/chunker-smoketest.ts"
|
||||
ln -s "$REPO_ROOT/node_modules" "$BUILD_DIR/node_modules"
|
||||
|
||||
# Build a minimal smoketest binary that imports the chunker. We compile this
|
||||
# instead of the full gbrain CLI so the failure mode is laser-focused on
|
||||
# chunker + WASM path resolution, not unrelated CLI wiring.
|
||||
bun build --compile --outfile "$OUT_BIN" scripts/chunker-smoketest.ts >/dev/null 2>&1
|
||||
if ! (cd "$BUILD_DIR" && bun build --compile --outfile "$OUT_BIN" scripts/chunker-smoketest.ts >/dev/null); then
|
||||
echo "[check-wasm-embedded] FAIL: bun could not compile the smoketest binary." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run it and capture JSON output.
|
||||
OUTPUT="$("$OUT_BIN" 2>&1)"
|
||||
|
||||
+1
-1
@@ -350,7 +350,7 @@ if [ -f .git ]; then
|
||||
fi
|
||||
|
||||
echo "[ci-local] Running checks inside runner container..."
|
||||
docker compose -f "$COMPOSE_FILE" run --rm "${EXTRA_MOUNTS[@]:-}" runner bash -c "$INNER_CMD"
|
||||
docker compose -f "$COMPOSE_FILE" run --rm "${EXTRA_MOUNTS[@]}" runner bash -c "$INNER_CMD"
|
||||
|
||||
echo ""
|
||||
echo "[ci-local] All checks passed."
|
||||
|
||||
@@ -52,6 +52,7 @@ import { isUndefinedColumnError } from '../core/utils.ts';
|
||||
// drift from what search actually filters.
|
||||
import { resolveHardExcludes, DEFAULT_HARD_EXCLUDES } from '../core/search/source-boost.ts';
|
||||
import { escapeLikePattern, buildVisibilityClause } from '../core/search/sql-ranking.ts';
|
||||
import { hnswIndexExpected, hnswMaxDimsForType } from '../core/vector-index.ts';
|
||||
|
||||
export interface Check {
|
||||
name: string;
|
||||
@@ -6147,6 +6148,12 @@ export async function buildChecks(
|
||||
continue;
|
||||
}
|
||||
if (engine.kind === 'postgres' && haveIndex.get(colName) === false) {
|
||||
if (!hnswIndexExpected(entry.type, entry.dimensions)) {
|
||||
okColumns.push(
|
||||
`${colName} (exact scan: ${entry.type}(${entry.dimensions}) exceeds HNSW cap ${hnswMaxDimsForType(entry.type)})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
issues.push(
|
||||
`${colName}: no HNSW index. Search works but uses sequential scan. ` +
|
||||
`Fix: CREATE INDEX IF NOT EXISTS idx_chunks_${colName} ON content_chunks USING hnsw (${quoteIdentifier(colName)} ${entry.type}_cosine_ops);`,
|
||||
|
||||
@@ -34,6 +34,11 @@ export function hnswMaxDimsForType(columnType: 'vector' | 'halfvec'): number {
|
||||
return columnType === 'halfvec' ? PGVECTOR_HNSW_HALFVEC_MAX_DIMS : PGVECTOR_HNSW_VECTOR_MAX_DIMS;
|
||||
}
|
||||
|
||||
/** Whether pgvector can build an HNSW index for this exact column shape. */
|
||||
export function hnswIndexExpected(columnType: 'vector' | 'halfvec', dims: number): boolean {
|
||||
return dims <= hnswMaxDimsForType(columnType);
|
||||
}
|
||||
|
||||
export function applyChunkEmbeddingIndexPolicy(sql: string, dims: number): string {
|
||||
return sql.replaceAll(CHUNK_EMBEDDING_HNSW_INDEX, chunkEmbeddingIndexSql(dims));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
chunkEmbeddingIndexSql,
|
||||
applyChunkEmbeddingIndexPolicy,
|
||||
PGVECTOR_HNSW_VECTOR_MAX_DIMS,
|
||||
PGVECTOR_HNSW_HALFVEC_MAX_DIMS,
|
||||
hnswIndexExpected,
|
||||
checkActiveBuild,
|
||||
dropZombieIndexes,
|
||||
dropAndRebuild,
|
||||
@@ -32,6 +34,15 @@ describe('chunkEmbeddingIndexSql — pre-v0.30.1 contract', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hnswIndexExpected', () => {
|
||||
test('matches pgvector caps for vector and halfvec columns', () => {
|
||||
expect(hnswIndexExpected('vector', PGVECTOR_HNSW_VECTOR_MAX_DIMS)).toBe(true);
|
||||
expect(hnswIndexExpected('vector', PGVECTOR_HNSW_VECTOR_MAX_DIMS + 1)).toBe(false);
|
||||
expect(hnswIndexExpected('halfvec', PGVECTOR_HNSW_HALFVEC_MAX_DIMS)).toBe(true);
|
||||
expect(hnswIndexExpected('halfvec', PGVECTOR_HNSW_HALFVEC_MAX_DIMS + 1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyChunkEmbeddingIndexPolicy', () => {
|
||||
test('replaces the canonical index SQL', () => {
|
||||
const input = `BEFORE\nCREATE INDEX IF NOT EXISTS idx_chunks_embedding ON content_chunks USING hnsw (embedding vector_cosine_ops);\nAFTER`;
|
||||
|
||||
Reference in New Issue
Block a user