← PRA / report example

Independent demo based on public GitHub data. PRA is not affiliated with or endorsed by Typesense, Inc.

Typesense, September 2026

typesense/typesense · v31 branch · 2026-09-01 to 2026-10-01, UTC

Executive read

Most of the month’s work corrects searches that returned the wrong result set. A search across several collections dropped documents it had already counted. Natural language search ran the user’s sentence instead of the query built from it, so a question answered by filters returned nothing. Phrase and infix searches ignored a filter_by that matched no documents. Two changes are measured: a selective filter from 31 ms to 1 ms, and an embedding call from a 471 ms median to 179 ms.

Key developments

1. Union search returns every document it counts

A search across several collections ranked results through a key that combined the collection id and the document id by hashing them. The hash collides on small values, so two documents in neighbouring collections were read as one and one was discarded. The found count stayed complete, no cutoff flag was set, and the client saw a total that did not match the pages. The key now carries both ids and is used for pinned hits too, so a pinned document no longer suppresses an unrelated one.

Sources: #3035 · review, section 1.

2. Phrase and infix searches keep a filter that matched nothing

When a filter_by matched no documents or timed out, phrase and infix search fell back to their unfiltered matches. The path is reachable when a pinned or hidden hit, facets or grouping stop the search from returning early, so a request could answer with the documents the filter excluded. Both paths now keep such a filter and admit nothing.

Sources: direct commit d4dbdae, regression tests in #3051 · review, section 5.

3. Natural language search uses the generated query

A model turns a sentence into search parameters, but retrieval ran on the sentence itself: it was passed in a hidden parameter that outranked the generated query. A wildcard query with a filter, the usual shape of a question answered by filters, returned no hits (#2910). The generated query now drives retrieval and the sentence is kept for highlighting.

Sources: #3047 · review, section 2.

4. A selective conjunction no longer pays for its broad side

filter_by=A && B cost whatever its wider side cost: both were computed in full and then intersected. A conjunction cannot return more documents than its narrower side, so when the other is at least 32 times wider the narrow side is computed alone and the wide side is only asked about the ids it produced.

On the project’s benchmark corpus the author measured the conjunction at 31 ms before and 1 ms after, a mixed filter at 8 ms and 2 ms, with single-leaf controls unchanged. The measurements are the author’s, on one machine. The plan needs a wide side not yet computed when the conjunction starts, so numeric filters in the default configuration, geo, id and filters over a referenced collection are unchanged.

Sources: #3049 · review, section 4.

5. A rejected alias swap keeps the previous target

An alias points at a collection, and a join can be declared through it. Swapping an alias to an incompatible collection returned an error and looked rolled back, yet after a restart the alias pointed at the rejected target, which broke join queries. The queue that replays writes after a restart applied the alias change before the collections referring to it, so there was nothing to validate against. Such changes are now ordered behind the collections that depend on them.

Sources: #3055 · review, section 7.

6. A remote embedding field survives a restart

A collection can declare a field whose embeddings come from a remote model. On startup the server pinged that endpoint and treated a failure as fatal, so a restart while it was unreachable removed the field from the schema (#2988). When the dimensions are already stored the ping adds nothing and is now skipped, and endpoint problems surface on requests to the model. The same pull request rejects a query embedding whose size does not match the field: a 400 instead of mismatched vectors in the index.

Sources: #3058 · review, sections 8 and 9.

7. Outbound calls reuse connections

Calls to remote models and other external endpoints opened a new connection every time, repeating DNS resolution and the TLS handshake. Non-streaming calls now lease connections from a pool with shared DNS and TLS session caches, while streaming and server-sent-event calls keep their own short-lived handles. The author measured an embedding call at a 471 ms median before and 179 ms after, with new TCP connections dropping from five to zero, and natural language search at 944 ms against 778 ms. The streaming case still opens its connections, and its slowest run got slower.

Sources: #3057 · review, section 12.

Open considerations

Async references and aliases. Alias ordering is fixed, the work on reference registrations is not: restoring missing registrations and removing one together with its field (#3082, 7 commits), and skipping collections that failed to load instead of creating ghost documents (#3066, 8 commits). While they are unmerged, a schema change that touches a reference can leave a stale registration behind.

Durability of bulk import. The month fixed what a search returns, not what a write stores: the branch where a seq_id becomes searchable before the document is durable (#3050, 7 commits) was open at the end of September.

Natural language search. Retrieval uses the generated query and the prompt is configurable. The branch with the most September commits, 14, is the typed judgment provider with reranking (#3072), and it is unmerged, so the shape of that generation may still change.

Filter cost and memory. The conjunction plan was measured by its author on two filter shapes. Work on the memory used by nested numeric range filters, in the same iterator code (#3063, 5 commits), is open, and a memory figure would set the boundary of the result.

Facet identifiers. The pinned-hit case is fixed. The collision between a sentinel and a real facet id (#3075) has been open since 24 September and was still open at the end of the month.

Outside the month’s themes. Also open at the cutoff: tokenizers for EmbeddingGemma and Qwen3 (#3038, #3039) and moving the bundled OpenSSL to 3.5.8 (#3064). None of them had merged by the end of September.

Other

The rest of the month’s integrated work. The schema prompt became configurable through five parameters with its previous defaults, and nl_query and nl_model_id started working inside a stored preset (#3046, section 3). Reference facets stopped losing the name of their collection under pinned hits, which could hang a search (#3084, section 6). Long synonyms sharing a prefix stopped matching the wrong term (#3054, section 10), and a geopolygon query takes the polygons whose cells it shares rather than all of them (#3061, section 11).

Privacy

Typesense is a trademark of Typesense, Inc.