displace / ext-turbovec
PHP 8.3+ native, in-process vector indexing and ANN search via TurboQuant.
Package info
github.com/DisplaceTech/ext-turbovec
Language:Rust
Type:php-ext
Ext name:ext-turbovec
pkg:composer/displace/ext-turbovec
Requires
- php: ^8.3
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-29 18:54:05 UTC
README
Native vector search for PHP, in-process.
Quantized ANN indexing over millions of embeddings — no vector database, no sidecar, no remote API.
Status
Feature-complete for its scope. Maintained: dependency updates and
upstream engine bumps ship as patch/minor releases — see the
CHANGELOG. Last verified: PHP 8.3/8.4/8.5 (NTS on macOS arm64 + Linux x86_64; ZTS 8.4/8.5 on Linux), turbovec 1.1.2, 2026-10-29.
Remaining ideas are tracked as future issues.
Co-maintainers welcome — if you run this in production and want a
say in where it goes, open an issue or a PR.
What is ext-turbovec?
ext-turbovec is a PHP 8.3+ extension for in-process vector indexing and
approximate-nearest-neighbor search. It is the retrieval half of a fully
local RAG stack for PHP: ext-infer
generates the embeddings, ext-turbovec indexes and searches them. No
Python sidecar, no vector database, no data leaving the machine.
Under the hood it binds the turbovec
Rust crate by Ryan Codrai — an implementation of Google Research's
TurboQuant quantization algorithm
(Gollapudi et al., arXiv:2504.19874):
data-oblivious 2–4-bit compression with near-optimal distortion, no training
phase, and SIMD search kernels (NEON on ARM, AVX2/AVX-512BW on x86) that
beat FAISS IndexPQFastScan on most configurations. All credit for the
index engine goes upstream — this extension is a downstream bindings
consumer, not a fork.
- 🗜️ 4-bit vectors — a 10M-document corpus that needs 31 GB as float32 fits in ~4 GB.
- ➕ Online ingest — add vectors and they're searchable; no train step, no rebuilds.
- 🔍 Filtered search — pass an id allowlist and the SIMD kernel honors it directly; selective filters get faster, not less accurate.
- 🆔 Stable ids —
IdMapIndexaddresses vectors by your own uint64 ids (SQL primary keys), with O(1)remove(). - 💾 Persistence — versioned on-disk formats (
.tv/.tvim) that round-trip search results bit-exactly. - ⚡ In-process — search latency is the kernel time; there's no IPC or network in the loop.
Quick start
pie install displace/ext-turbovec # or: make build && php -d extension=...
<?php use Displace\Vector\IdMapIndex; $index = new IdMapIndex(dim: 1024, bitWidth: 4); // Vectors enter as packed float32 strings — pack('g*', ...) output. $index->addWithIds( pack('g*', ...$embeddingA) . pack('g*', ...$embeddingB), [101, 102], // your ids, e.g. SQL primary keys ); $result = $index->search(pack('g*', ...$queryEmbedding), k: 10); foreach ($result as $row) { printf("doc %d scored %.4f\n", $row['id'], $row['score']); } $index->write('corpus.tvim'); // ...and IdMapIndex::load() it back
Pair it with ext-infer for end-to-end semantic search — see
examples/semantic-search.php, or
examples/basic-search.php for a standalone,
model-free walkthrough.
The packed-vector contract
Index methods accept vectors as packed little-endian float32 binary
strings — exactly what PHP's pack() produces with the g format code:
$packed = pack('g*', ...$floats); // one vector $batch = $packedA . $packedB . $packedC; // batches are plain concatenation
strlen($payload) must be a whole multiple of 4 * $dim; anything else
throws DimensionMismatchException. This is deliberately the only input
path — packed strings cross the FFI boundary as a single memcpy-sized blob
instead of millions of PHP zvals, and it's the format ext-infer emits
natively: $model->embed($text)->packed() (ext-infer 0.2+) goes straight
into add()/search() without touching a PHP array. For array-minded
code, Vectors::pack(array $floats): string and
Vectors::unpack(string $packed, int $dim): array are the on-ramp.
Memory math
The index stores bitWidth bits per coordinate plus one float32 scale per
vector. For 100,000 documents at dim 1024 with 4-bit quantization:
100,000 × 1024 × 4 bits = 51.2 MB (codes)
100,000 × 4 bytes = 0.4 MB (scales)
─────────
~52 MB vs 410 MB as raw float32
bitWidth: 2 halves the codes again when recall requirements allow.
dim must be a positive multiple of 8, at most 16384 (every common
embedding size qualifies: 384, 512, 768, 1024, 1536, 3072, 4096, …).
Measured: that 100K × 1024 index adds 52 MB of resident memory after
load() and a first search.
API surface
| Class | Methods |
|---|---|
Displace\Vector\TurboQuantIndex |
__construct(int $dim, int $bitWidth = 4) · add(string $vectors) · count() · search(string $query, int $k = 10): SearchResult · write(string $path) · static load(string $path) |
Displace\Vector\IdMapIndex |
as above, plus addWithIds(string $vectors, array $ids) · search(string $query, int $k = 10, ?array $allowlist = null) · remove(int $id) |
Displace\Vector\SearchResult |
ids(): array · scores(): array · count(): int · Countable · IteratorAggregate yielding ['id' => int, 'score' => float] |
Displace\Vector\Vectors |
static pack(array $floats): string · static unpack(string $packed, int $dim): array |
Exceptions: VectorException (extends \RuntimeException) with
InvalidArgumentException, DimensionMismatchException, and
IndexIOException beneath it. Full reference with semantics at
turbovec.displace.tech.
Compatibility
| macOS arm64 | Linux x86_64 | Linux arm64 | Windows | |
|---|---|---|---|---|
| PHP 8.3 | ✅ | ✅ | ✅ | — |
| PHP 8.4 | ✅ | ✅ | ✅ | — |
| PHP 8.5 | ✅ | ✅ | ✅ | — |
CPU-only by design. On x86_64 the AVX-512 kernels engage automatically
when the CPU has them, with an AVX2 fallback (Haswell 2013+) below.
ZTS is enabled in composer.json and CI runs the PHPT suite on ZTS PHP
8.4/8.5 (Linux) weekly; release binaries are NTS-only.
No runtime libraries to install on any platform: since 0.2.0 the
upstream engine is pure Rust (0.1.x needed libopenblas0 on Linux).
This release pins upstream turbovec 1.1.2.
The pin is exact because the on-disk index formats must stay stable;
upstream bumps are deliberate, documented events (see RELEASE.md).
Upgrading from 0.1.x? Rebuild your index files — 0.1.x files use a
format 0.2 can't read or convert; see
Persistence.
Roadmap
Shipped TurboQuantIndex · IdMapIndex with allowlist
filtering and O(1) remove · packed-vector fast path · Vectors helpers ·
write/load persistence · typed exceptions · PHPT suite · CI matrix ·
PIE-compatible binary releases · packed handoff from ext-infer's
Embedding::packed() · turbovec 1.x: half the memory, ~3× faster
search, no OpenBLAS (0.2.0).
Remaining ideas (batch search, mmap-backed load(), range search,
…) live as future issues, each with its
reasoning. PLAN.md holds the design notes and decision
history.
Deliberately out of scope Windows · GPU · graph indexes (HNSW-style) — TurboQuant's flat quantized scan is the sweet spot this extension is built around.
Part of a stack
Transcribe → chunk → embed → search → answer, entirely on your hardware:
| Stage | Package | What it does |
|---|---|---|
| Transcribe | ext-whisper | Speech-to-text over whisper.cpp |
| Chunk | ai-toolkit | Text chunkers and packed-vector math, pure PHP |
| Embed · rerank · answer | ext-infer | Local LLM chat, embeddings and reranking over llama.cpp |
| Search | ext-turbovec (this repo) | Compressed in-memory vector search (TurboQuant) |
| Integrate | ai-contracts | The interfaces frameworks code against |
| Reference app | rag-over-markdown | The whole pipeline over a folder of markdown |
Every package links the others; the extensions share one scaffold, ext-template.
Writing about this
- ext-turbovec: A Vector Index Inside Your PHP Process
- Meaning as Geometry: What Embeddings Actually Are
- Seeing in 1,024 Dimensions: How UMAP Makes Embeddings Visible
- From Keywords to Concepts: Semantic Search in Laravel with pgvector
The stack's launch series ran Sept 22 – Oct 1, 2026; everything else is at eric.mann.blog/writing.
License
MIT © 2026 Eric Mann / Displace Technologies. Bundles turbovec © Ryan Codrai, MIT — see THIRD-PARTY-NOTICES.md. TurboQuant is the work of Gollapudi et al., arXiv:2504.19874.