← All extensions

Vectors

ivyx

Turns your files into a searchable index that Knowledge and semantic search build on

Vectors

Turns your files into a searchable index and answers questions with the nearest passages from it. It has no panel of its own — the surfaces belong to the extensions built on top of it.

What you can do

  • Get semantic search that finds a passage by meaning, not just by matching words
  • Have it back the Knowledge catalog, the assistant's project context, the Semantic toggle in search, and capability matching in the generation pipeline
  • Keep the whole thing on your machine by choosing an on-machine embedder, or send text to a remote one when you would rather
  • Rebuild an index incrementally as your files change, rather than from scratch
  • Get an answer even when the index is missing or the embedder stopped responding, because the query falls back to keyword search and says so

There is no vector database to run and no RAG framework here. It produces and serves an index over your own material.

Getting started

Install it and it starts working through whatever consumes it — turn on the Semantic toggle in search, or open the Knowledge catalog. Choose your embedder in the Vectors settings first if you want everything to stay on this machine.

What is in this version

The embedder, the index and the query. vectors.embed with its configuration, a self-test that runs the whole pipeline over one notebook, vectors.index.build with incremental rebuilds, removal and per-corpus collections, and vectors.query. The design is in docs/vectors.md and the ordering is in the fleet's WORK/2-vectors.md.

The query

vectors.query embeds a question and returns the nearest passages. It never fails into nothing: with no index, no embedder, or an endpoint that stopped answering, it falls back to workspace keyword search and comes back labelled keyword with the reason attached. Consumers render one list and show the label. Keyword hits presented as semantic ones is the one way retrieval can lie, so the mode is never inferred and the reason is never swallowed.

corpus takes a list, because a notebook's markdown was routed to the document corpus while its code stayed in the code one, and asking for a single corpus would lose half of the same file. The scores merge because every collection named belongs to the same embedding space by construction.

There is no default score floor. Cosine values from hash and from a real model are not on the same scale, so a threshold tuned on one is wrong on the other. Pass minScore if you have your own evidence.

Each hit carries the whole passage in text and a capped one-line preview. An assistant putting a hit in front of a model needs the passage; a truncation is not recoverable.

Two consumers ship with it. A toggle in the search panel, which is a mode of the existing search box rather than a second panel. And capability matching in the generation pipeline, where node documents and step queries are embedded in one batch up front so the matcher's inner loop stays arithmetic, and where the scorer stays off on the hash embedder because a spurious similarity there can only promote a wrong node.

The index

Call vectors.index.build with a corpus name and the paths that belong to it. That list is the corpus: unchanged files are skipped, and files it no longer names have their chunks deleted. Dead passages that survive a rebuild come back as results, so removal is part of building rather than a separate clean action. Pass prune: false for a partial build, and know that it stops.

Notebooks split by cell. Everything else gets an overlapping line window, so a passage straddling a boundary is still reachable from one side.

A notebook's markdown cells do not compete with its code cells. They go to the document corpus instead. That is measured, not stylistic: with everything in one ranking, a two-line LaTeX note was the top hit for two unrelated queries, and separating them took retrieval from 3/5 to 4/5.

A build writes once per collection. The default store rewrites a whole collection on every write, so writing per file costs 2.4 GB where one flush costs 11.8 MB, for the same index. That is what npm run measure:vectors reports, and it is why no vector database is involved.

Embedders

Configured in Settings, under Vectors.

Source What it does What it costs
hash Deterministic on-machine hashing. The default Free and offline. Matches on shared words, not on meaning, so it proves the pipeline runs rather than that retrieval works
endpoint Posts the text to a URL you configure Real quality. Every call sends your material off this machine
local Runs a real embedding model on this machine Real quality with no network. Needs the desktop app and a model runtime extension

local is the only setting under which the index never leaves the machine and the results are still semantic. Getting there is two steps: install the Ollama Runtime extension, then ollama pull nomic-embed-text (or call llms.ollama.model.pull). Set the model name and 768 as the dimension and it is on. There is no address to configure and no token, because nothing is sent anywhere.

An endpoint is expected to answer with a list of vectors, { data: [{ embedding }] } or { embeddings: [...] }. Its token is named, not pasted: put the value in the SECRETS panel and write the entry's name in vectors.endpoint.secretRef.

Sending text off the machine

A remote embedder turns "index my workspace" into sending the workspace somewhere. So:

  • The address is shown before the first send of a session, with how many texts and how many characters are going. Changing the address asks again.
  • The default embedder is never remote.
  • Nothing is embedded automatically. Not on save, not on drop. Ingestion is always something you asked for.
  • vectors.embedder.status reports the address without sending anything.

Embedder identity

Vectors from two different models are not comparable, and cosine similarity will not tell you that — it returns confident nonsense. So identity is part of the address: a collection is named <corpus>-<modelId>-<dim>. Changing the model or the vector length reads and writes a different collection, the old vectors stay on disk until you clear them, and no query can cross embedding spaces.

The configured dim is checked against the first vector that comes back. A mismatch is an error, because it means the settings describe a different model than the one answering.

Capabilities

Id What it does
vectors.embed Embed a batch of texts. The only embedding path in the product
vectors.chunk Split a file into the units that get embedded
vectors.index.build Chunk, embed and store a set of files
vectors.index.upsert Store records a caller produced itself
vectors.index.status What is indexed, per corpus, with bytes on disk
vectors.query Embed a question and return the nearest passages, labelled by mode
vectors.index.clear Drop an index
vectors.embedder.status Which embedder, which space, which address. Sends nothing
vectors.embedder.test One real call, to see whether the endpoint answers
vectors.probe The self-test below

Every signature is plural. A capability call traverses the whole gateway pipeline, so embedding one chunk at a time would put governance in an inner loop.

The self-test

vectors.probe reads one notebook, treats each cell as a chunk, embeds them, stores them in a throwaway collection, asks a question worded unlike the cell that answers it through vectors.query, and reports whether the right cell came back. Then it clears the collection. It goes through the shipped query rather than around it, because a self-test that reimplements the path it tests only tests the reimplementation.

It runs the same words through keyword search as a control. Without that, "semantic retrieval found it" is an assertion; with it, it is a comparison. On the hash embedder the result says plainly that the run proved the pipeline works and nothing about meaning — and it means that literally. On one notebook hash passed a paraphrase query because two unrelated tokens hashed into the same bucket, beating the cell that actually shared a word with the query. A single probe run on hash is not evidence in either direction.

Three commands, three different questions:

  • npm run check:vectors drives the shipped modules in Node against a stubbed platform and a real loopback server. 153 assertions over the manifest, the embedder, the resolution point, the endpoint client, the on-machine source, the egress disclosure, the chunker, the builder, the query and the probe. It answers is the code the shape it claims to be.
  • npm run probe:vectors runs the money test against a live engine over this repo's own notebooks. It answers does a real model retrieve on meaning, and nothing stubbed can. It also runs the same model through the local source, using the runtime extension's own build output rather than a stub, so the two columns are expected to report the same number.
  • npm run measure:vectors runs the write-cost measurement. It answers does the default store run out, which decides whether a vector database is ever worth adding.
Vectors: Run Retrieval Probe
  path:   notebooks/features.ipynb
  query:  the cell that normalises the data
  expect: StandardScaler

License

MIT