Eric Mann just wrote up ext-turbovec, a PHP 8.3+ extension for vector indexing and approximate nearest neighbor search that runs inside your PHP process. You store embeddings, search them, and get IDs back, with no vector database, no Python sidecar, and no data leaving the machine.

That's the difference from the usual options for a PHP app. Pinecone is a hosted service you call over the network, and pgvector keeps your vectors in PostgreSQL, so both put the index in a separate system you query. ext-turbovec keeps the index in your PHP worker's memory.

The engine underneath is the turbovec Rust crate by Ryan Codrai, which implements Google Research's TurboQuant quantization algorithm. The extension is bindings to that crate, not a fork.

Here's what it gives you:

  • 4-bit vectors so a large corpus fits in memory
  • No training step, so vectors are searchable as soon as you add them
  • Your own IDs through IdMapIndex, so results map straight back to database rows, with remove() when content is deleted
  • Filtered search that takes an ID allowlist
  • Persistence to versioned on-disk files with write() and load()
  • SIMD kernels on ARM and x86, using AVX-512 when the CPU has it and falling back to AVX2

The Memory Math

Keeping vectors in process only works if they're small. Raw float32 embeddings at 1,024 dimensions take 410 MB for 100,000 documents. ext-turbovec stores 4 bits per coordinate plus one scale per vector, which brings the same corpus to about 52 MB. At 10 million documents, that's roughly 4 GB instead of 31 GB.

You can drop to bitWidth: 2 to halve it again if your recall requirements allow it.

Adding and Searching Vectors

Here is the quick start from the README:

use Displace\Vector\IdMapIndex;

$index = new IdMapIndex(dim: 1024, bitWidth: 4);

$index->addWithIds(
    pack('g*', ...$embeddingA) . pack('g*', ...$embeddingB),
    [101, 102],
);

$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');

Vectors go in as packed float32 strings, which is what pack('g*', ...) produces, and a batch is just those strings concatenated. That's the only input format. Passing a million vectors as PHP arrays means tens of millions of zval allocations, while a packed string crosses into the extension as one block of memory. If your code works with arrays, Vectors::pack() and Vectors::unpack() convert for you. A payload that isn't a whole multiple of the dimension throws a DimensionMismatchException.

Filtered Search

Real apps rarely search everything. There's usually a tenant, a status, or a permission check first. IdMapIndex::search() takes an optional allowlist of IDs, and the search kernel skips anything not on it.

So you can run a cheap SQL query to build the list of candidate IDs and let the index rank within them. You don't lose results the way you do when you filter after searching, and a selective filter makes the scan faster because there's less to scan.

What It Doesn't Do

The extension is CPU only, with no GPU support and no Windows builds. It also skips graph indexes like HNSW on purpose. It does a flat scan over the quantized vectors, and Mann's argument is that TurboQuant's scan is fast enough into the millions of vectors that not building, tuning, and repairing a graph is the better trade.

It pairs with ext-infer, Mann's extension for generating embeddings, to keep a whole retrieval pipeline in PHP. The roadmap for v0.2 lists batch search, mmap-backed loading, and a direct handoff of packed embeddings from ext-infer.

Installing It

ext-turbovec installs with PIE and ships prebuilt binaries for macOS arm64 and Linux x86_64 and arm64, on PHP 8.3, 8.4, and 8.5:

pie install displace/ext-turbovec

On Linux, the engine links OpenBLAS, so you'll need the libopenblas0 package at runtime. On macOS there's nothing extra to install. The current release is v0.1.0, and the README marks the project as pre-release.

Read Mann's announcement post and the documentation for the full API, and find the source on GitHub and Packagist.