Search by

displace / ext-turbovec

ericmann

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

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 4

v0.2.1 2026-10-29 18:53 UTC

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.

CI Latest release PHP 8.3 / 8.4 / 8.5 MIT License Documentation

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 — IdMapIndex addresses 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

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.