Knowledge-Base Engineering

The Hidden Schema of Embeddings in HexClaw: Migrating a Textbook’s Index from 768 to 4096 Dimensions

From Nomic’s 768 dimensions to Qwen’s 4096: manage execution contracts, budgets, dual index revisions, resumable rebuilds, and CAS cutover separately. The old index serves valid unchanged content while its replacement builds in the background.

First published on WeChat Originally published in the official technical column of the WeChat account 「hexclaw」 (ID: hexclaw) · Engineering notes on embedding-index migration
Original on WeChat ↗

Switching a knowledge base’s embedding model sounds like a configuration change: replace the model name, adjust the dimension, and restart. Once that knowledge base contains a real textbook, however, the change becomes less straightforward.

While developing K12 parent tutoring for HexClaw v0.5.0, the knowledge base became more than a place to upload a few references. A child’s profile links to specific textbooks and passes the current unit and page range into the tutoring pipeline. One is a 131-page PDF, Mathematics, Grade 5, Second Semester, currently scoped to Unit 4, pages 45–62. Generating aligned exercises and supporting tutoring both require evidence from that textbook.

During the same iteration, we changed the built-in default for local knowledge-index embeddings from nomic-embed-text to Ollama’s qwen3-embedding:8b. Both can be downloaded through HexClaw’s Ollama management interface. The existing textbook already had a 768-dimensional Nomic index. The new model produces 4096 dimensions and needs a task instruction on the query side. The textbook must remain searchable after the upgrade. We changed the model; parents should not inherit the homework of uploading their textbooks again.

Start with this local migration. At first glance, changing the model name and dimension really does look like two lines of configuration:

default_model = "qwen3-embedding:8b" dimension = 4096 # old: 768

After those two lines, the work has only begun. Old vectors do not upgrade their own dimensions. Existing documents need new embeddings. Batch size and timeout budgets must be reconsidered for an 8B local model. Retrieval-score distributions change too, so relevance thresholds require recalibration with real textbook samples.

One boundary matters: v0.5.0 also supports cloud deployment, but where the backend is deployed and where the embedding model runs are separate choices. A locally deployed backend can call a cloud embedding service. User configuration and knowledge-base policy select the model. Wherever it runs, the system must define how to build the new index, when the old index hands over, and which revision an in-flight query reads.


Core argument

The textbook’s original text can remain. Changing an embedding model involves three separately managed concerns:

① How vectors are generated: the model, dimensions, and input transformations form an execution contract. Under the current conservative policy, changing that contract creates and rebuilds a new index revision.

② How rebuilding runs: items per batch, total runes, and timeouts belong to the execution budget. Adjusting them changes scheduling, without recalculating committed vectors.

③ How relevant a result must be: relevance thresholds belong to retrieval policy. They require evaluation and calibration, without recalculating vectors.

Keeping these concerns separate prevents a timeout adjustment from turning into a full-library recomputation.

30-second version

A 131-page Grade 5 mathematics textbook illustrates a local index migration from Nomic’s 768 dimensions to Qwen’s 4096. HexClaw retains the original text and separates old and new vectors using versioned indexes. While the old executor remains available, the old index continues serving unchanged content whose generation is still valid; the new index rebuilds in background batches. Durable jobs and batch manifests recover known progress, without blindly retrying calls whose outcome is unknown. A CAS transaction checks policy, content version, lease, and build state before atomically switching the read entry point. Relevance thresholds are calibrated separately against real samples. A local or cloud backend does not determine where the embedding model runs.

“Ingested” is not a single state

First, separate what “the textbook has been ingested” means. In code, a parent’s single statement corresponds to three different committed facts.

Scroll horizontally to see every table column.

Boundary Committed facts What becomes available Behavior on failure
Acceptance transaction Original file, source, document record, background job Return 202; progress can be queried Not searchable yet
Text publication Document, text chunks, full-text index (FTS), content version Full-text retrieval and page references Job fails or waits for retry; incomplete text-processing output does not enter knowledge-base retrieval
Semantic publication / cutover A document becomes ready in an index revision, or a completed revision takes over the read entry point A new document enters the active space, or the new space takes over queries The active index serves valid old content; explicit search can fall back to text

This is not unique to local knowledge bases. OpenAI File Search combines semantic and keyword retrieval into a hosted tool. When files enter a vector store, the platform handles chunking, embeddings, and indexing. Official documentation still lists some indexing operations as asynchronous, requiring completion waits or status checks.[1] A successful upload and a retrieval-ready index are different things. A hosted service takes on some of the work, but the state boundary remains.

In HexClaw’s path, the first two steps handle the original file and text. Vector indexing is the third step, and the subject here.

PDF parsing retains the original and page-level checkpoints so completed pages can be reused after interruption. The complete text enters knowledge-base retrieval only after completion checks. The rest of this article starts with text, full-text indexing, and page references already available; it does not expand on ingestion.


Dimensions are explicit; the execution contract is the hidden schema

Physical incompatibility is obvious in a move from 768 to 4096 dimensions: a length check can stop a Qwen query vector from being compared with Nomic document vectors. But dimensions reveal only the first layer. Switching to another 4096-dimensional model, or merely changing the query instruction while keeping Qwen, may still make old vectors unsuitable for direct reuse.

A vector is a function’s output. The function is determined by the combination of model artifact, input transformation, and execution protocol. Two maps printed on 4096 squares do not necessarily share coordinates. Matching dimensions are a prerequisite for a similarity calculation; the model and input transformations must match too.

Qwen’s official asymmetric-retrieval recipe is a clear example: encode documents directly, but prepend a task instruction to queries.[3] HexClaw fixes its own task description using that template. In this prefix, the original query follows Query: directly, without an added space:

Instruct: Given a search query, retrieve relevant passages that answer the query Query:<original query>

The document prefix remains empty. The instruction applies only to query embeddings; it does not change the textbook text or FTS5. Adding the same instruction to both queries and documents may look like harmless consistency, but changes the document-side input that is embedded.

Prompt Engineering in HexClaw treats instructions to a model as interface contracts. This query instruction is part of such a contract: it changes the input used to generate a vector, rather than the tone of an answer.

The public model name is Qwen/Qwen3-Embedding-8B; Ollama’s exact model ID is qwen3-embedding:8b. The official model supports Matryoshka representations from 32 to 4096 dimensions.[3][6] HexClaw currently fixes 4096 dimensions in its execution Profile. Changing the dimension creates the next index revision, rather than mixing vector lengths within one revision.

Execution contracts extend beyond HexClaw. Consider the requirements vendors place on vector generation and compatibility:[7][8]

Scroll horizontally to see every table column.

Where the contract lives Official examples What the system must retain
Input role Qwen query instructions; E5 query/document prefixes; Cohere input_type; Jina v3 Task LoRA Actual query/document input bytes and task identity
Representation BGE-M3: dense / sparse / multi-vector Output mode, score meaning, and retrieval algorithm
Output and numerical conventions Some OpenAI / Qwen / Jina / Cohere models support reduced or selectable output dimensions, under differing rules Dimensions, dtype, normalization, truncation order, and implementation version
Compatibility across generations Gemini 001 → 2 requires full recomputation; the Voyage 4 family shares a space Assume incompatibility unless the contract explicitly guarantees it

Even with the same model, implementation changes can alter vector generation. Weaviate’s property ordering, field concatenation, and case handling all affect model input. Jina v3 also corrected the order of normalization and dimensional truncation; its official guidance requires pinning historical code to reproduce older results.[7][12] The model name can stay unchanged while the generation conditions change.

How HexClaw records the contract: 12 fields and a fingerprint

HexClaw records each embedding execution configuration as a credential-free contract in cmd/hexclaw/semantic_index_runtime.go:

type knowledgeEmbeddingExecutorContract struct { ContractVersion string Dimension int DocumentPrefix string EndpointIdentity string ExactModelID string MaxInputRunes int Normalization string Protocol string ProviderInstanceID string ProviderLocation string QueryPrefix string TransformEpoch string }

The 12 fields are normalized into JSON and hashed with SHA-256 to produce profile_config_hash. profile_id identifies which model on which provider; the configuration fingerprint distinguishes the execution configuration it uses. The former may remain unchanged when the endpoint, dimension, or input transformation changes; the latter must change.

Queries match currently registered executors by configuration fingerprint. If no executor with the same contract is available, the vector route is disabled. Today’s default model must not interpret yesterday’s vectors. This fingerprint identifies configuration; it does not restore an old model or historical runtime.

This is conservative. Changing only a query instruction or moving the same model to another endpoint may not mathematically require new document vectors. HexClaw nevertheless includes those changes in the fingerprint and rebuilds a new revision when applying the policy. It may perform extra computation in exchange for a clear mapping between execution configuration and index revision.

One gap remains: these 12 fields do not separately record a model-artifact digest, quantization level, or inference-engine version. The artifact behind an unchanged tag can change without changing the configuration fingerprint.

Not every change warrants a full rebuild

Manage the conditions for generating vectors, the budget for scheduling computation, and the rules for filtering results separately:

Layer Typical fields Action after a change
Execution contract Exact model ID, dimensions, query/document input transformations, normalization declaration Current conservative policy: new hash, new revision, rebuild
Execution budget Items per batch, total runes, call timeout Reschedule without recalculating committed vectors
Retrieval policy Top-K, relevance threshold, automatic injection enabled or disabled Re-evaluate without changing the vector space

Deterministic truncation and prefixes change the text sent into the model, so they belong to the execution contract. How identical inputs are packed into requests and how much time they receive belong to the execution budget. Confusing these categories can omit a necessary rebuild or make a timeout adjustment recompute the entire knowledge base.

An unchanged model name does not guarantee an unchanged index either. Anthropic’s September 2024 Contextual Retrieval adds a chunk’s original context before building embedding and BM25 indexes.[2] The model is the same, but its input text has changed. If HexClaw adopts such processing later, the added context and generation rules must also be recorded, or old and new index-generation conditions will be impossible to reconcile.


A batch of 64 timed out. How should the budget change?

The first successful connection in July used Ollama 0.32.5, qwen3-embedding:8b Q4_K_M, and CPU execution. HTTP 200 returned three 4096-dimensional vectors for three inputs without calling a chat model. A working call does not mean a whole textbook can be processed in one shot.

When processing textbook batches, the old “64 items, 60 seconds” configuration timed out. Those settings had served lightweight embeddings and could not simply be carried over to this CPU environment.

We then limited both item count and runes. A rune is a Unicode code point in Go: it measures input scale here, not model tokens. These were some recorded single-call durations:

Historical single observation Duration
Cold-start query · original input36.290s
1 item per batch71.989s
2 items per batch48.602s
4 items per batch200.853s
8 items per batch302.655s

These are single observations retained from 2026-07-28. The original per-call logs and complete hardware, runtime settings, and cold/warm order were not fully retained. The figures explain the tuning process at that time; they cannot compare batch sizes, establish throughput, or predict a full-textbook rebuild duration.

In those records, batch 1 was slower than batch 2, but that does not establish that batch 2 is generally faster. Startup, scheduling, input length, and memory state all affect elapsed time. The resulting Qwen Profile limits both input scale and execution time:

item_count <= 2 total_runes <= 800 max_input_runes <= 400 batch_timeout = 120s query_timeout = 60s

The Worker first limits item count. The executor wrapper then greedily packs requests by both item count and total runes: if the next item would exceed a limit, it sends the current batch first.

The query-side 60 seconds is shared by the entire vector-retrieval stage. Resource waits and model calls for expanded queries all spend that budget. If an upstream context expires sooner, execution ends earlier. It is not a separate allowance for each expanded query, and excludes the preceding query-expansion and subsequent reranking stages.

These settings apply only to the exactly matching Qwen Profile. Nomic’s prefixes, threshold, and budgets remain unchanged. Other models require their own input requirements and budgets to be confirmed.

Inputs exceeding 400 runes are currently truncated, not split into smaller sub-chunks. Original text and FTS remain complete, but a vector may not encode the whole chunk. Query text receives its instruction before truncation, so that prefix also consumes the 400-rune allowance.

A model’s 32K context support does not establish how many seconds a request will need. Fitting and finishing are separate budgets. Changes to hardware, model artifact, or provider require the execution budget to be checked again.


How can one BLOB column manage two index generations?

The early local knowledge base used a simple design: SQLite held original text and chunks, FTS5 handled keyword search, and a BLOB column on the chunk table held embeddings for Go to compare with cosine similarity. For a single-model, single-machine knowledge base, it was simple, transparent, and avoided operating another vector service.

An online migration temporarily requires two vectors for the same chunk: Nomic A continues retrieval while Qwen B rebuilds beside it. Adding model_name to the old table is insufficient: each chunk still has room for only one generation. Overwriting rows is worse. At 63% rebuilt, the read path would face an incomplete index that can no longer be restored in place.

Database migration v23 separates the source of truth from its derived index. Four identities must be distinguished: revision identifies an entire semantic-index generation; document generation identifies a document’s content generation; content version tracks corpus changes; and policy version tracks user-policy changes. Changing the model, replacing a textbook, and selecting a different target cannot share one ambiguous “version.” The key fields are:

kb_semantic_corpora.active_revision_id -> A kb_embedding_policies.desired_revision_id -> B
kb_index_revisions (revision_id, profile_snapshot_id, publish_state, base_content_version, indexed_through_version, ...)
kb_revision_vectors (revision_id, document_id, content_generation, chunk_id, profile_config_hash, model_name, dimension, embedding)
CHECK(length(embedding) = dimension * 4)

The corpus’s current content_version lives on kb_semantic_corpora. Each revision retains its starting base_content_version and its caught-up indexed_through_version. These identify where it started and how far it has caught up.

The old kb_chunks.embedding column remains as a legacy field, but the new path no longer updates it. Reverting to an older program therefore cannot recover a complete semantic index for content added during migration from that column.

Each new vector row carries its revision, configuration fingerprint, model, dimensions, and document generation. Composite foreign keys constrain revision/configuration ownership, while BLOB-length checks constrain vector shape. They cannot prove that the provider used the right artifact or that retrieval is sufficiently relevant.

The original textbook and chunks remain in the source of truth. Embeddings are derived data that can be recomputed. Migration leaves the original text intact while old and new index generations track their own states.

For Cloudflare’s BGE-M3, for example, an OpenAI-compatible interface permits request-protocol reuse, but vector space, budget, and thresholds still require model-specific confirmation. BGE-M3 itself supports dense, sparse, and multi-vector representations.[8][9] HexClaw’s current retrieval path uses dense-vector recall fused with FTS/BM25 results; it does not yet integrate that model’s sparse or multi-vector retrieval.

Retaining an old index until its replacement is ready is not unique to HexClaw. Qdrant’s official migration guidance likewise requires re-embedding original text: old vectors are not reversible encodings of that text and cannot be directly converted into a new model’s vectors. It also warns that concurrent deletions during blue-green backfilling can let an old point be “resurrected” by a background scroll. Elasticsearch uses an atomic alias swap to replace the read entry point.[12] HexClaw follows that read-entry-switching approach. Document content generations, Worker leases, and model-call outcomes remain application responsibilities even if the vector store changes.

In Desktop, separate model capability from current index state. Qwen3-Embedding-8B is recognized as embedding-only and does not appear among chat, Quick Chat, or default conversation model candidates. Index cards use the backend’s policy_version + active_revision + desired_revision + indexing_activity as authoritative state. Applying a policy includes the version previously read. A 409 triggers a fresh backend read rather than replaying a potentially stale write.

A changed default model does not automatically migrate a knowledge base

Making Qwen the built-in default does not migrate existing knowledge bases on restart. Local Ollama model selection prioritizes explicit configuration, then probes installed models when none is specified, and uses the built-in default only as a final fallback. Existing knowledge bases retain their own policies. Discovering a new model only adds an option. The system creates a target index only when the user applies a new selection. Downloaded, selected by policy, and actively serving queries are three separate states.


Old A keeps serving while new B rebuilds beside it

After migration begins, Nomic revision A remains the only semantic-index read entry point. Qwen revision B is desired / staged and builds only in the background. B does not participate in queries at 1%, 63%, or even 99% complete.

Queries first load the active snapshot, then filter vectors by the same revision, configuration fingerprint, dimension, and document generation. If the old executor is still available, A continues semantic retrieval for unchanged documents whose generations remain valid. If the old model has been deleted, the vector route stops. Explicit knowledge-base searches can still return FTS5 / LIKE results, while automatic RAG injection in chat returns nothing rather than presenting keyword-only hits as successful vector recall.

Local Nomic-to-Qwen migration: active serves valid old content while desired rebuilds from text and takes over through CAS

Figure 1 | Local Nomic → Qwen migration: the old index serves valid old content while the new index rebuilds in the background and takes over through CAS.

Resuming an interrupted rebuild

Laptops sleep when their lids close, desktop apps exit, and local models can stay busy for extended periods. New-index rebuilding therefore runs as a durably recoverable Job.

After claiming a job, a Worker receives execution authority through a Lease, valid for 5 minutes by default. Those 5 minutes are the validity period of its badge, not a deadline for completing the textbook. The Worker renews before each batch and again after the model returns, before committing. A lease epoch identifies the authority generation, distinguishing the current Worker from a late older one.

Retryable failures back off from 30 seconds exponentially, capped at 15 minutes. The default permits at most 8 consecutive attempts without progress; progress resets the counter. This does not mean an entire textbook is limited to 8 batches.

Each model request is still split into small Qwen Profile batches: at most 2 items and no more than 800 runes in total.

Before each call, the Worker writes a durable Manifest recording the batch’s identity and processing state: revision, configuration fingerprint, chunk order, content digest, stable request key, and lease epoch.

After the model returns, it checks vector count, 4096 dimensions, finite components, content generation, and Lease. If those checks pass, the vectors, Manifest’s succeeded state, and job progress commit in the same transaction.

The scan cursor exists only in memory for the current round. After restart, scanning begins again, skipping completed work through matching vectors already stored in the database. Batches with unknown in-flight outcomes receive separate handling.

Anthropic’s Managed Agents engineering article of 2026-04-08 also places session logs outside the execution harness so a crashed process can recover.[10] This article concerns index Jobs rather than agent sessions, but shares the principle: durable state need not live and die with one process.

A timeout—why not just send it again?

Not so fast. The provider may already have executed the request, with its response lost during a timeout or dropped connection. The system records this as outcome_unknown and does not blindly retry it automatically. A stable Idempotency-Key identifies the request, but actual deduplication depends on provider support. It cannot guarantee exactly-once execution.

An unknown outcome currently fails that Job. It does not enter ordinary backoff, and there is no general automatic reconciler. Recomputing usually does not modify remote business data, but may occupy the local model again or incur duplicate cloud charges.

What if the user clicks Retry? For failed jobs with an explicitly selected Profile and Desktop retry support, the system reapplies policy and creates a new staged revision and Job. Known successful vectors can be reused when both identity and content match. An old unknown receipt does not thereby become successful; the unknown portion may still be recomputed.

Harness Engineering in HexClaw discusses failure classification. Index rebuilding also must distinguish a definitively failed call from a call with an unknown outcome. They cannot share one retry policy.

Users keep changing material during a rebuild

Users do not stop working while the index rebuilds. Additions, deletions, and other changes still need handling; reparsing the same-source textbook cannot currently run concurrently with index migration. HexClaw does not compute both A and B vectors for every change. That saves one computation, at the cost of delaying semantic access to new content.

While target B works normally, it processes current document generations and catches up with the corpus content version. A keeps only unchanged old content with valid generations readable. Replacing a textbook filters out vectors from the old generation. Explicit text retrieval can see new content first; semantic retrieval of that content waits until B takes over.

Cancellation, deletion, and rollback

Cancelling migration cancels B, abandons the staged revision, and creates incremental Jobs to fill A’s migration-window gaps, all in the same transaction.

Deleting a document first records a soft-delete and tombstone, advances its generation to invalidate old execution authority in in-flight Jobs, and then uses durable garbage collection to clean derived data. However much work an old Worker has done, an expired badge cannot authorize a commit. The database recognizes only currently valid execution authority.

Cancelling B before cutover and filling A’s gaps is not cost-free rollback after cutover. Once B becomes active, superseded A no longer tracks new writes. Retaining an old revision does not guarantee a lossless switch back; migration-window data and compatibility still need handling.


100% progress is not the cutover condition

B finishing all vectors proves only that its Worker reached the end of the queue. Before the last batch returned, the target policy could have changed from B to C, the textbook’s generation could have changed, or another Worker could have taken the Lease. Letting a late “100%” change the read entry point can atomically activate a stale index. Atomicity guarantees all-or-nothing, not that the target is correct. These are concurrent backend states that must be defended against. Desktop currently disables that card’s model selection while a desired revision exists.

Cutover occurs in a CAS, compare-and-swap, transaction. Condition checks and commit happen in the same SQLite database. External model calls stay outside that transaction.

The transaction first checks policy and content versions plus the expected active and desired indexes, confirming the publication target has not changed. Job state, owner, lease epoch, and lease validity confirm execution authority. B’s staged state, caught-up content version, and expected, completed, and failed counts confirm the build state.

Only matching conditions mark A superseded, mark B active, and clear desired. Any mismatch rolls back the entire cutover.

// 简化伪代码;省略 Job、租约有效期、watermark 和完整性校验 if policyVersion != expectedPolicyVersion || contentVersion != expectedContentVersion || activeRevision != revisionA || desiredRevision != revisionB || leaseEpoch != expectedLeaseEpoch { return ErrPublishConflict }

What happens to queries already in flight when reads switch? HexClaw does not change their index revision mid-query. A retrieval freezes its revision at the start, and all expanded queries share that plan.

After multi-path retrieval and before fusion, it checks content_version again. A plain A→B cutover does not force an in-flight request to change revisions: its frozen A can continue as superseded. If the corpus version has changed, however, retrieval returns an evidence conflict rather than mixing old and new content into one result.

In-flight requests freeze A; new requests bind to B after background cutover. Old requests check content version before fusion and return an evidence conflict if it changed.

Figure 2 | Index cutover does not force in-flight requests to change revisions. Content version is checked after multi-path recall and before fusion; this does not guarantee global consistency throughout answer generation.

Implementation: FreezeRetrievalPlan and ValidateRetrievalPlan, in knowledge/knowledge.go and knowledge/semantic_revision_search.go.

So far, these mechanisms establish index completeness and valid concurrent state at cutover. CAS does not score the new model’s retrieval accuracy; relevance thresholds require their own calibration.


Nomic’s 0.85 cannot simply be copied to Qwen

Nomic’s previous automatic-injection threshold was 0.85, derived from Chinese samples on a real machine at that time. Qwen’s cosine distribution differs. Even if both use (cosine + 1) / 2 to map scores into 0–1, the same score from different models does not imply the same relevance. Qwen therefore needs its own automatic-injection calibration.

We did not use another LLM to label the evaluation set. Instead, expected source matches were fixed in advance using frozen K12 textbooks and repository samples, then the real Ollama path was called. A small calibration set retained from 2026-07-28 contained 7 relevant queries and 5 queries with no target. The original per-query records were not fully retained. These results informed tuning at the time; they are neither an independent validation set nor an overall recall measurement:

Relevant queries had Top-1 and Top-3 hits of 7/7, with a minimum normalized Top-1 score of 0.6940. No-target queries had a maximum normalized Top-1 score of 0.6132.

For those samples, 0.65 lay between the two score groups, so the Qwen Profile’s automatic-injection threshold was set accordingly. That number belongs to the model and input transformation used then, not the global MinScore.

A false retrieval: raising the threshold from 0.65 to 0.68

The following targeted examples checked relevance boundaries across textbooks, beyond the opening Unit 4 tutoring scenario.

Later investigation found that “What is the meaning of life?” incorrectly retrieved the language-arts textbook passage The Mystery of Life in the Universe at about 0.66225. Qwen’s automatic-injection threshold was therefore raised to its current 0.68. Two real application requests on September 21 confirmed zero knowledge hits for that question, while “What are the factors of 18?” still cited page 11 of the mathematics PDF and answered correctly. This changed retrieval policy, without recomputing vectors or changing the configuration fingerprint. It demonstrates only those targeted examples, not a general quality guarantee.

Qwen currently injects at most 1 item. This is a final count limit after fusion, relevance filtering, and reranking/MMR, not an early restriction to the highest-cosine candidate. Both 0.68 and the one-item cap are specific local-Qwen policies, not general quality thresholds for other models or providers.

Looking back at July’s calibration, relevant English samples had a minimum first-to-second margin of just 0.0072, while no-target samples could reach 0.0288. In those examples, a larger margin did not imply stronger relevance. The observations were insufficient to justify a universal margin gate, so none was added then. Candidate quality should instead be improved through corpus / subject scope, BM25 recall, RRF fusion, and reranking. Do not temporarily lower a relevance threshold just to make recall look larger.

The noise-control pipeline in Context Engineering in HexClaw has an upstream prerequisite: fusion, thresholding, and reranking can filter candidates only after vector retrieval actually works, with matching execution contracts on queries and documents.

CAS currently checks structural completeness and concurrent state; targeted samples calibrate Qwen’s relevance threshold. The next step is connecting shadow queries and a fixed golden set to publication so quality results also participate in cutover decisions.


The cost of 4096 dimensions: 5.33 times the raw storage per vector

After cutover, old indexes leave more questions: how long to retain them, when to reclaim them, and which data to fill during rollback. Automatic retention and reclamation still need further work.

Increasing dimensions from 768 to 4096 increases disk and memory requirements too. A raw float32 Nomic 768-dimensional vector is 3072 bytes, or 3 KiB; a Qwen 4096-dimensional vector is 16 KiB. Single-generation storage and per-dimension cosine arithmetic both grow by roughly 5.33 times. During the two-generation migration window, vector payload alone is 19 KiB per chunk.

Scale Nomic 768 Qwen 4096 Dual-generation window
Per chunk 3 KiB 16 KiB 19 KiB
100,000 chunks About 293 MiB About 1.53 GiB About 1.81 GiB
1,000,000 chunks About 2.86 GiB About 15.26 GiB About 18.12 GiB

These figures count only float32 vector payload. They exclude SQLite rows, indexes, Manifests, WAL, text, legacy columns, and additionally retained revisions, so the dual-generation window is only a capacity lower bound. Decoding 64 Qwen vectors into float32 arrays uses 1 MiB. A JSON response also includes numeric encoding and structural overhead; that figure is not its actual HTTP body size.

The current search layer: a full scan, not ANN

Vector search currently has no approximate nearest-neighbor index, ANN. SQLite returns all eligible vectors within the selected scope and the revision bound to the retrieval plan. Go decodes each, calculates cosine similarity, performs a complete stable sort, and takes topK. Time complexity is approximately O(N·D + N log N), where N is candidate-vector count and D is dimension.

Decoded candidate vectors and text remain in memory. Vector payload requires O(N·D) memory, with candidate text stored in addition; memory is not limited to O(N) scores.

SQLite keeps text, index metadata, and publication state coordinated in one database without first requiring another vector service. The tradeoff is growing scan and memory cost as candidate count increases. Earlier capacity estimates say nothing about query latency at a million chunks, nor do they establish retrieval quality.

A future Qdrant, Elasticsearch, pgvector, or Faiss integration could upgrade physical search. The application would still control Profiles, desired revisions, durable Jobs, content catch-up, and CAS. Changing storage does not settle those engineering responsibilities automatically.


Recovering from failures

The failure windows and their corresponding recovery or rejection actions are:

Scroll horizontally to see every table column.

Failure window Durable state Recovery / rejection
Worker exits during processingCommitted Manifests and vectorsReclaim with a new execution epoch; skip committed work and handle unknown in-flight outcomes separately
An old Worker returns lateCurrent lease owner / epochErrJobFenced rejects the old Worker’s commit
Model-call outcome unknownManifest = outcome_unknownFail this Job; no blind automatic resend or automatic reconciliation
Text succeeds, embedding failsDocument / Chunks / FTS are readyResume only the vector Job; do not rerun PDF processing
Textbook replaced while B buildsNew generation / content versionB catches up; an old-snapshot CAS fails
Old active executor unavailableText facts and old revision identityExplicit FTS remains available; automatic RAG returns nothing

An honest account of this version’s boundaries

Following this series’ convention, here are the implemented mechanisms and remaining boundaries:

  • Model-artifact identity is not yet part of the configuration fingerprint. The 12 fields omit the model digest, quantization level, and inference-engine version. An artifact behind an unchanged tag can change without changing the fingerprint.
  • The quality gate is not yet integrated with publication. CAS checks structural completeness and concurrent state, not the new model’s retrieval quality. Shadow queries and a fixed golden set are the next step.
  • These samples were for calibration, not independent validation. There were 7 relevant and 5 no-target queries, without fully retained per-query originals. Qwen’s injection threshold was first 0.65, then 0.68 after targeted investigation. They describe different stages of the Qwen Profile. Sample hits are not an overall recall rate, and the thresholds cannot be directly generalized to other models.
  • Latency figures are historical single observations. Retained from 2026-07-28 without complete original call logs, hardware, runtime parameters, and cold/warm order, they cannot compare batch sizes or establish throughput or full-textbook rebuild time.
  • The current Qwen Profile truncates inputs beyond 400 runes. Original text and FTS remain complete, but vectors may not encode whole chunks. Query instruction prefixes also consume that 400-rune allowance.
  • outcome_unknown has no automatic reconciliation yet. It fails the Job rather than entering normal backoff. Recomputation usually does not change remote business data, but may occupy a local model again or incur cloud charges.
  • There is no cost-free rollback after cutover. Once B is active, superseded A stops tracking new writes. Automatic retention and reclamation of old indexes also need further work.
  • Semantic retrieval of new content waits for B’s cutover. Explicit text search sees additions and replacements first during rebuilding. Reparsing a same-source textbook and migrating its index cannot currently run at the same time.
  • Search uses a full scan, not ANN. The capacity figures are only vector-payload lower bounds. They neither establish latency at a million chunks nor prove retrieval quality. Whether to introduce ANN or a separate vector store depends on whether real corpus size and query cost have made full scans a bottleneck.

The next sequence is to strengthen model-artifact identity and quality-sample traceability, then address unknown-outcome recovery, old-revision retention, and rollback. That work is not all complete.


Back to the textbook: keep the text, rebuild vectors under the new contract

From 768 to 4096 dimensions, the original text stays while derived vectors rebuild under a new execution contract. Versioned indexes separate active and desired spaces, durable Jobs retain known progress, and a CAS transaction checks conditions before switching reads. Whether results are relevant enough still requires real questions.

Changing two configuration lines is only the start of a model migration.
Record generation conditions in the contract, compute limits in the budget, and validate relevance with real questions.
Then migration has an index workflow that can be resumed and checked.


Earlier articles · HexClaw series

▸ K12 Engineering Practice in HexClaw
Verification evidence and platform boundaries through the tutoring scenario—the textbook context in this article’s opening.
▸ Prompt Engineering in HexClaw
Model instructions as interface contracts—the query instructions discussed here.
▸ Harness Engineering in HexClaw
Execution budgets, controlled failures, and outcome checks—the budget and unknown-outcome concerns here.
▸ Context Engineering in HexClaw
Filtering answer-like noise from recalled candidates—the fusion, filtering, and reranking after an index is ready.

References

  • [1] OpenAI · File Search / Retrieval—hosted retrieval, automatic chunking and indexing, and asynchronous file operations; checked 2026-10-13.
    developers.openai.com/api/docs/guides/tools-file-search
    developers.openai.com/api/docs/guides/retrieval
  • [2] Anthropic · Contextual Retrieval—published 2024-09-19; embedding / BM25 indexing after adding chunk context; checked 2026-10-13.
    anthropic.com/engineering/contextual-retrieval
  • [3] Qwen · Official Qwen3-Embedding-8B model card—4096 dimensions, MRL, and query instructions.
    huggingface.co/Qwen/Qwen3-Embedding-8B
  • [4] QwenLM · Official Qwen3-Embedding repository and technical report.
    github.com/QwenLM/Qwen3-Embedding
    arxiv.org/abs/2506.05176
  • [5] Qwen · Official Qwen3 Embedding and Reranker blog post.
    qwenlm.github.io/blog/qwen3-embedding/
  • [6] Ollama · Official qwen3-embedding model page and tags.
    ollama.com/library/qwen3-embedding
    ollama.com/library/qwen3-embedding/tags
  • [7] Embedding vendors’ model contracts: E5, Jina v3, OpenAI, Cohere, Gemini, and Voyage 4.
    huggingface.co/intfloat/multilingual-e5-large
    huggingface.co/jinaai/jina-embeddings-v3
    openai.com/index/new-embedding-models-and-api-updates/
    docs.cohere.com/docs/cohere-embed
    ai.google.dev/gemini-api/docs/embeddings
    blog.voyageai.com/2026/01/15/new-models-and-expanded-availability/
  • [8] MTEB · Tasks and evaluation methodology; BGE-M3 · Dense / Sparse / Multi-vector contracts.
    docs.mteb.org/overview/
    bge-model.com/bge/bge_m3.html
  • [9] Cloudflare Workers AI · BGE-M3 and the OpenAI-compatible embedding interface.
    developers.cloudflare.com/workers-ai/models/bge-m3/
    developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/
  • [10] Anthropic · Scaling Managed Agents: Decoupling the brain from the hands—2026-04-08; separating session logs from execution processes; checked 2026-10-12.
    anthropic.com/engineering/managed-agents
  • [11] RFC 8785 · JSON Canonicalization Scheme.
    rfc-editor.org/rfc/rfc8785
  • [12] Vector-migration practice: Qdrant model migration, Elastic aliases, and Weaviate vectorization.
    qdrant.tech/documentation/tutorials-operations/embedding-model-migration/
    elastic.co/docs/manage-data/data-store/aliases
    docs.weaviate.io/weaviate/model-providers/openai/embeddings
  • [13] HexClaw / HexClaw Desktop · Embedding execution configuration, index lifecycle, and Desktop policy controls.
    github.com/hexagon-codes/hexclaw
    github.com/hexagon-codes/hexclaw-desktop

This article follows HexClaw and HexClaw Desktop source implementations. Source and previously included external materials were checked through 2026-10-12. Added OpenAI retrieval and Anthropic Contextual Retrieval materials were checked on 2026-10-13.


Whether the backend is local or in the cloud, selecting cloud embeddings sends document chunks and query text to the selected provider. Hosting your own knowledge base does not make every operating mode fully offline.