Independent demo based on public GitHub data. PRA is not affiliated with or endorsed by Typesense, Inc.
Repository typesense/typesense, branch v31,
window [2026-09-01T00:00:00Z, 2026-10-01T00:00:00Z). Data
snapshot 11 November 2026.
Changes were selected by merge date, taken from the committer date of
the squash commit in the v31 history and checked against
the pull request it closes. Author dates of branch commits were not used
for selection, because a branch can carry September dates and still not
be part of v31 at the cutoff.
Twelve commits were part of v31 at the cutoff and
entered it inside the window: eleven pull requests and one direct commit
by a maintainer. One further commit, b8363a3807, carries a
23 September committer date but was not in the branch on 30 September,
so it is left out. Coverage of the window is complete against the branch
history.
Tests were read in the diffs, not run. Where a pull request author
reports a run, this review says so and names the author as the source.
Code links are pinned to the merge revision, so behaviour on current
v31 may differ.
The newest published release is v30.2 from 19 April 2026 (releases). No commit in this window is in a published release.
Before: Union_KV entries were keyed by
hash_combine(collection_id, seq_id), or
(search_index, seq_id). hash_combine is not
injective over small integers. The pull request gives
(3, 1000) and (4, 932) as a colliding pair,
and any two documents in neighbouring collections whose sequence ids
differ by about 64 collide the same way. The topster treated a colliding
pair as one document and kept one, while found was computed
elsewhere and stayed correct. No cutoff flag was set, so the response
gave no indication of loss. The curated skip inside
Collection::do_union computed
hash_combine(collection_id, distinct_key) independently and
had the same collisions, which let a pinned document suppress an
unrelated raw document from another collection.
After: both halves of the key are 32-bit, so they are packed into the
64-bit key rather than hashed. Union_KV::get_key is used
for both insertion and the curated lookup.
Code: 245ebc7,
include/topster.h and src/collection.cpp.
Tests read: UnionTest.ReturnsEveryHitAcrossCollections,
UnionTest.CuratedHitShouldNotSuppressUnrelatedRawHit,
UnionTest.DynamicFacetMinOccurrenceRatioShouldApplyAfterUnionMerge,
plus test/topster_test.cpp.
Limits: the diff changes key construction and the tests cover selected cases. This review did not independently check every ranking, merge-order or facet case in a union.
Before: the natural-language path wrote the user’s sentence into a
hidden raw_query parameter, and
collection_search_args_t::init preferred
raw_query over q. Retrieval ran on the
sentence, so a model-generated q="*" combined with a
filter_by returned no hits. Issue #2910
reports that case. The existing test HighlightOriginalQuery
asserted the buggy result.
After: raw_query and its RAW_QUERY constant
are removed, and init reads q. The sentence is
carried as original_nl_query and used only for
highlighting. Its tokens are added as exact highlight candidates when
they exist in the field index. When the generated q is
empty or *, the sentence becomes the highlight text and
infix matching is disabled for it.
Code: c0f2899,
include/collection.h, src/collection.cpp,
src/natural_language_search_model_manager.cpp.
Tests read: HighlightOriginalQuery was removed and
replaced by
NaturalLanguageSearchModelManagerTest.FilterOnlyQueryHighlightsOriginalText,
.GeneratedQueryControlsRetrievalAndHighlightExpansion and
.WildcardGeneratedQueryReturnsFilteredBooks.
Limits: highlighting of a filter-only query now comes from the sentence, which is a deliberate behaviour change. The pull request does not measure how often generated queries differ from the sentence in practice.
Before: generate_schema_prompt() held the facet limits
as literals: 20 values fetched per facet field, 10 listed in the prompt,
20% sampling above 1000 documents, and all string facet fields included.
The prompt cache was keyed by collection alone.
After: schema_prompt_params_t holds
nl_max_facet_values, nl_schema_sample_values,
nl_facet_sample_percent,
nl_facet_sample_threshold and nl_facet_fields.
They can be set per request, per multi_search item, or in a
preset. Defaults equal the previous literals, so existing prompts are
byte-identical. The cache is keyed by collection plus parameters and
tracks the collection per entry, so clear_schema_prompt()
drops every variant for that collection.
schema_prompt_params_t::parse() rejects caps above a 10000
ceiling and rejects an nl_facet_fields entry that is
missing, unfaceted or not a string.
The same pull request fixes preset reading, which is a separate
defect. Presets were read expecting a value wrapper that
put_upsert_preset() never writes, so nl_query
and nl_model_id stored in a preset had no effect. A preset
is now read as the stored search parameters object, and preset merging
fills only parameters the request left unset, which restores the
documented precedence of embedded over request over preset.
Code: 2d09752,
include/natural_language_search_model_manager.h,
src/natural_language_search_model_manager.cpp.
Tests read: SchemaPromptFacetValueCaps,
SchemaPromptFacetFieldsSelection,
SchemaPromptCachedPerPromptParams,
SchemaPromptCacheExpiryOnCollectionAlter,
GetNLQueryParamsFromPreset,
RequestParamsWinOverPreset,
InvalidPresetFacetLimit, NonScalarPresetValue,
PresetFacetFieldsMissingFromCollection,
MissingCollection.
Limits: a larger facet cap means a longer prompt and more tokens per call. The pull request documents the parameters and the ceiling, and does not measure answer quality at higher caps.
Before: compute_iterators() materialised both subtrees
of an && and intersected the results, so the cost
was bounded by the wider side regardless of what the conjunction
matched. The construction-time estimate min(left, right) is
the right size for the result, but says nothing about the cost of
producing it.
After: when one side is at least AND_PROBE_RATIO times
wider, the narrow side is materialised alone and the wide side is asked
about each id it yields through the is_valid(id) call that
and_filter_iterators() already uses to advance a lazy
subtree. Ids come out in the same order, so the node’s result, its
validity and approx_filter_ids_length are unchanged.
computed_by_probe records which plan ran, exposed through
_get_computed_by_probe() so a regression that quietly
returns to materialising both sides fails a test.
The merged code sets AND_PROBE_RATIO to 32, asserted by
ASSERT_EQ(32, AND_PROBE_RATIO) in the same diff. The pull
request description still says 8 and describes a boundary test at 39
against 40 ids, while the merged test documents 159 against 160, which
is 32 times a narrow side of 5. The description is stale relative to the
merged code, and the code value is the one reported here.
Scope of the new plan: it needs the wide side to still be lazy when
its parent && is initialised. That holds for any
string leaf above string_filter_ids_threshold in every
configuration, and for an integer or float leaf when
enable_lazy_filter is on. A range-index, geo or
id:* leaf has already materialised inside its own
init, so probing it is correct but cannot recover a cost
paid one level down. Both sides are still materialised when either
subtree filters on a referenced collection, because intersecting drops
an id whose reference results have nothing in common and probing has no
equivalent step. The same holds when the node is an object filter root,
and when the wide side is an operator rather than a leaf.
Timeouts: both children receive a copy of the budget before anything
is computed, the counted check inside is_valid cuts the
probe loop short, and a forced check after the loop covers a loop too
short to reach the clock. A -1 from is_valid
is not read as a timeout, because in a lopsided &&
it almost always means the wide side ran out of ids, so
validity distinguishes the two cases.
Conditions, from the pull request description: Apple M1 Max, 10
cores, 64 GB, macOS 26.6.2, release builds via
bazel build //:typesense-server with -O2,
origin/v31 against the branch, the same on-disk index,
single request, no concurrency. found is identical in every
row.
On musicbrainz-1M-songs, the corpus the repository’s own
benchmark/ directory downloads:
| scenario | found | before | after |
|---|---|---|---|
filter_selective_and, added by the pull request |
431 | 31 ms | 1 ms |
filter_complex, existing |
1,258 | 8 ms | 2 ms |
filter_simple, single leaf, control |
214,468 | 15 ms | 15 ms |
| one broad leaf alone, control | 960,372 | 61 ms | 61 ms |
On a synthetic set of two million documents with one field matching 100 documents and one matching 1,900,000:
filter_by |
found | before | after |
|---|---|---|---|
selective && broad |
95 | 92 ms | 1 ms |
broad && selective |
95 | 92 ms | 1 ms |
| broad leaf alone | 1,900,000 | 137 ms | 145 ms |
| ratio 8x between sides | 1 | 0 ms | 0 ms |
| ratio 1000x between sides | 5 | 5 ms | 0 ms |
| numeric wide side, default config | 95 | 37 ms | 38 ms |
The author attributes the broad leaf’s 137 to 145 ms to run-to-run noise measured minutes apart, and notes that a single leaf never reaches the operator branch that changed. The numeric wide side under the default configuration is the scope limit stated above. These are the author’s measurements on one machine. They were not independently reproduced for this review.
Code: f91b4a5,
include/filter.h,
include/filter_result_iterator.h,
src/filter_result_iterator.cpp.
Tests read: FilterTest.AndProbeSelectiveSide,
.AndProbeRatioBoundary, .AndProbeNumericSide,
.AndProbeKeepsReferences, .AndProbeTimeout,
.InfixLazyEvaluation,
CollectionFilteringTest.SelectiveAndDoesNotMaterializeWideSide,
.MissingFilterLazyEvaluationSearchHits.
Before: Index::do_phrase_search applied the filter only
when filter_result_iterator->validity was truthy. A
filter that was provided but matched no documents, or that timed out,
fell through to the branch where the phrase matches themselves became
the filter, so the response contained documents the filter excluded.
Index::do_infix_search had the same gap. Both paths are
reachable when curation, facets or grouping stop the search from
returning early.
After: the phrase path checks validity != invalid and,
when the filter was provided but empty, drops the phrase ids and returns
an empty result instead of falling back. A timed-out iterator is passed
through add_phrase_ids so the combined iterator keeps the
timeout state and fails closed. The infix path skips a field when the
iterator is not valid and a filter was provided. Infix result ownership
was reworked in the same commit, which had been mixing a borrowed
pointer into infix_ids with an owned array from
ArrayUtils::exclude_scalar.
Code: direct commit d4dbdae,
src/index.cpp.
Tests read, from the commit itself and from the follow-up pull
request #3051 (0e35bc0):
CollectionSpecificTest.PinnedHitDoesNotAdmitPhraseMatchesWhenFilterMatchesNothing,
.HiddenHitDoesNotAdmitPhraseMatchesWhenFilterMatchesNothing,
.FacetsExcludePhraseMatchesWhenFilterMatchesNothing,
.GroupedHitsExcludePhraseMatchesWhenFilterMatchesNothing
and the four infix counterparts, plus
UnionTest.SearchCutoffKeepsInfixMatchesFiltered.
Limits: the direct commit carries no message body, so the reasoning above comes from the diff and from the tests added with it and in #3051.
Before: Index::do_facets mutated orig_index
and reference_collection_name on the facet objects it was
given. During pinned-hit processing that cleared the metadata
identifying a joined reference facet, so the facet was misidentified and
a search could hang or return incorrect results. Reported as #3083.
After: the facet is copied before those fields are set.
Code: 20e294c,
src/index.cpp.
Tests read:
CollectionJoinTest.PinnedHitsPreserveReferenceFacetIdentity,
.PinnedReferenceFacetMetadataEdgeCases,
.GroupByWithVectorQueryDoesNotLeakReferenceFacets,
CollectionCurationTest.CurationIncludesPreservePinnedReferenceFacetIdentity,
and an HTTP-level test in
api_tests/tests/pinned_reference_facet.test.ts covering
multi-search, direct search and a reference-only facet request.
Before: an alias upsert had no identity in
BatchedIndexer, the queue that applies writes in order and
replays them after a restart. On replay an alias upsert could run before
the collections that reference the alias were created. With no reference
metadata available, validation was skipped and an invalid target was
persisted. The visible sequence was a swap to an incompatible collection
that returned an error, appeared rolled back, and then pointed at the
rejected target after a restart, which broke async-reference helper
resolution and join queries.
After: alias upserts are queued by alias name consistently at enqueue, replay and completion. A scheduler dependency is added from the alias to the requested target collection, and the existing reverse-reference dependency tracking makes alias updates wait behind collections that reference the alias. Dependency state is refreshed from the committed alias target after the request completes, so a failed swap keeps the old target relationship.
Code: 1726f4b,
include/batched_indexer.h,
src/batched_indexer.cpp,
src/collection_manager.cpp.
Tests read:
BatchedIndexerAliasTest.ResolvedAliasSchemaStillOrdersFailedSwapAfterChildCreation,
.FailedEarlierAliasRequestPreservesPendingTarget,
.EarlierCompletionPreservesEveryPendingAliasTarget,
.DeleteWaitsForBlockedUpsertAcrossSnapshotRestore,
.DeleteCompletionPreservesLaterUpsertTarget,
CollectionJoinTest.FailedAliasSwapKeepsAsyncReferenceTargetAfterRestart,
and the HTTP reproduction in
api_tests/tests/alias_async_reference_replay.test.ts.
The code calls an alias a symlink, as in
CollectionManager::get_symlinks(), while the API calls it
an alias.
Before: init_collection validated a remote embedding
endpoint on load and treated any validate_and_init_model
failure as fatal for the field, so a restart while the endpoint was
unreachable dropped the auto-embedding field from the persisted schema.
Reported as #2988.
After: when the field’s dimensions are already persisted
(num_dim > 0), the load-time ping guarantees nothing and
the embedder it would register is identical to one registered without
it, so remote embedders are registered directly and endpoint problems
surface on the requests that use them. A malformed persisted config
still fails construction and drops the field.
Code: 9c3d882,
include/embedder_manager.h,
src/embedder_manager.cpp,
src/collection_manager.cpp,
src/collection.cpp.
Tests read:
CollectionManagerTest.RestoreRemoteEmbeddingFieldWithoutEndpointValidation,
.RestoreAutoSchemaDocsOnRestart,
.RestorePresetsOnRestart.
Limits: the load-time check is skipped only when dimensions are persisted. A field whose dimensions were never written still goes through validation on load.
The same pull request as section 8 added a size check where query embeddings are consumed, in all three places. A remote model that starts returning a different dimensionality, for instance because the model behind a fixed URL was swapped, now fails the search with a 400 rather than feeding mismatched vectors into the vector index.
Code: 9c3d882,
src/collection.cpp.
Before: the synonym index is an adaptive radix tree that stores only
the first MAX_PREFIX_LEN bytes of a compressed intermediate
node’s prefix. During fuzzy traversal, bytes beyond the stored prefix
were treated as matching the query, so differences in the omitted part
went undetected. The pull request names the visible cases: long synonym
terms, synonym_num_typos: 0, multibyte scripts such as
Thai, and several synonym terms sharing a long prefix.
After: the fix stays outside the generic radix tree and applies to
synonym matching only. The tree still produces ordered candidate leaves,
and long candidates are revalidated against their complete leaf keys
using the same byte-level optimal string alignment distance, including
adjacent transpositions. synonym_prefix matches are
validated against candidate prefixes. Exact matches keep their fast path
and short-only synonym nodes keep the previous behaviour. The existing
cap of ten returned synonym children stands. When invalid long-prefix
candidates fill the initial window, the window widens until ten valid
matches are found, the candidates are exhausted, or every child has been
considered. Per-node metadata avoids scanning every synonym child per
query.
Code: 9214365,
include/synonym_index.h,
src/synonym_index.cpp.
Tests read:
CollectionSynonymsTest.LongCompressedPrefixFullKeyValidation,
.LongCompressedPrefixAndMultibyteValidation,
.LongCompressedPrefixCandidateCap,
.SynonymPrefix, .SynonymTypos,
.SynonymResolutionPreferExactMatch.
Before: GeoPolygonIndex looked S2 cell ids up through a
NumericTrie. The $-prefixed S2 index terms
parsed to cell id 0, which matched the trie root, so every query
shortlisted every indexed record. Reported as #3056.
After: a termToSeqids map is keyed directly on S2 index
terms, with per-term posting lists stored as ids_t compact
or full lists and ids_t::destroy_list cleanup when a term’s
list empties.
Code: f6f289c,
include/geopolygon_index.h,
src/geopolygon_index.cpp.
Tests read:
GeoFilteringTest.GeoPolygonTestRealCoordinates. The pull
request reports no measurement, so the effect is described as a change
in which candidates are considered, with no latency figure.
Before: each outbound call created a curl handle and discarded it, so DNS resolution and the TLS handshake were repeated per request. The pull request also notes that the bundled curl 7.87 carried a per-connect stall of about one second introduced by the 7.86 and 7.87 connection-filter refactor.
After: the bundled curl moves to 8.22 and non-streaming calls lease
handles from a LIFO pool. Handles return through
curl_easy_reset, which keeps the live connection, the DNS
cache and the TLS session on the handle. DNS and TLS sessions are shared
process-wide through curl_share lock callbacks, while
connections stay per handle because libcurl cannot share a connection
cache across concurrent threads. HTTP/2 is negotiated with an HTTP/1.1
fallback on HTTPS, and h2c prior knowledge is kept on plain HTTP. URL
schemes are now compared case-insensitively, so an HTTPS://
URL no longer skips the certificate authority and verification setup. A
comment in the diff records that the streaming and server-sent-event
paths keep their own short-lived handles, so the pool covers the
synchronous path.
Shutdown drains the pool: it waits for in-flight transfers, counts
unpooled handles in the drain, refuses new leases once the pool is down,
logs a drain that times out, and the HTTP server is deleted before
curl’s global cleanup. Stream waiters are notified when handle
initialisation fails. Auto-detected brotli and zstd are disabled in the
bundled build. SIGPIPE is blocked in the test server’s connection
threads so an SSL_shutdown alert to a closed socket cannot
terminate an isolated test.
Measurements reported by the author, five iterations per case with a
packet capture per phase, against OpenAI
text-embedding-3-small and gpt-4o-mini,
comparing v31 with curl 7.87 and no pooling against the
branch:
| scenario | median before | median after | new TCP connections before / after |
|---|---|---|---|
| embedding call | 471 ms | 179 ms | 5 / 0 |
| natural-language search | 944 ms | 778 ms | 5 / 0 |
| conversational SSE | 1,208 ms | 966 ms | 5 / 5 |
The author also reports that outbound reconnects dominated natural-language search at about 1,851 ms per call before the change and about 355 ms after. The SSE row keeps its five connections, which matches the short-lived handles on that path, and its slowest run moved from 1,284 ms to 1,660 ms. The author states that the isolated test and all ten pool tests were run with uncached Bazel runs. All of this is read as an author report, not reproduced here.
Code: e28bcec,
include/http_client.h, src/http_client.cpp,
src/typesense_server_utils.cpp,
test/http_client_pool_test.cpp.
This lists the branches that took commits in September and were not
part of v31 on 30 September. Seventy-one commits in all: 67
on the branches of twenty pull requests that were open at the cutoff,
and four on branches that had no open pull request on that date.
Author dates are used, which say when a commit was written and not
when it was pushed. Merges of v31 into a branch are left
out, so an initiative shows fewer commits here than GitHub does. Branch
state was restored retrospectively from git history and pull request
metadata, so exact draft or approval status on 30 September cannot be
attributed. None of this counts towards the month’s results, and the
list is not an estimate of readiness, a confirmed roadmap or a delivery
commitment.
Ordered by the number of September commits.
Continues the theme of section 2. The branch adds a provider that
assembles search parameters from typed model answers, a separate
reranking step, facet value shortlisting, per-round telemetry and a
total_timeout_ms budget. Opened 24 September. At the cutoff
this was the branch with the most September commits.
1544ccc3 2026-09-18 feat(nl-search):
add jev rerank and make the jev timeout a model config knob2bac61a7 2026-09-18 feat(nl-search):
add jev provider that assembles search params from typed
judgments3c3f6225 2026-09-18 fix(nl-search):
record the typed query in analytics instead of jev’s rewritten
q457e6c85 2026-09-18 feat(nl-search):
ship the jev word number and consumed check rounds off by
default658b600d 2026-09-18 feat(nl-search):
add jev client for typesafe system one6bbb2c2b 2026-09-18 feat:
add field-level description property to schema fields4da19b79 2026-09-19 feat(nl-search):
record per round jev telemetry and enforce a total_timeout_ms
budget1478ced7 2026-09-21 feat(nl-search):
implement facet value shortlisting for jev queries16988cf8 2026-09-21 fix(nl-search):
bound jev rerank by the request’s total_timeout_ms budget4dc2752d 2026-09-21 fix(nl-search):
abstain when a jev answer carries no confidence or distribution77d9375b 2026-09-21 feat(nl-search):
implement per-value role questions for facet selectionf39e47c8 2026-09-21 feat(nl-search):
fold consecutive numeric bounds into range clauses47bc1323 2026-09-22 chore:
remove dangling comment8c0707ed 2026-09-22 chore:
remove jev timing log lines from production buildA document became searchable before it reached the store. The branch
moves the store write ahead of publication, splits batch indexing into
phases and restores the alter_mutex scope across all of
them. One commit inside the branch reverts an earlier attempt at the
same thing. Opened 15 September.
4405e5d3 2026-09-15 fix:
release alter_mutex before cross-collection reference
propagation4c66edc4 2026-09-15 docs:
note pre-existing fields/alter_mutex gap found during #3028
review5fcb1d9d 2026-09-15 fix:
don’t strip store:false/nested fields before Phase 3 indexes
them63a2a0cd 2026-09-15 fix:
write documents to store before making them searchable in
batch_index81997ac3 2026-09-15 harden:
make batch_preprocess_records()/batch_finalize_memory_index()
private863de891 2026-09-15 fix:
restore alter_mutex lock scope across preprocess+finalize in
batch_indexa9303d08 2026-09-15 revert:
keep alter_mutex held through Phase 4 (fields race, not just
AB-BA)The branch tracks failures in failed_collection_loads,
answers 503 for an unavailable collection, rejects an incomplete load
before publication and rebuilds missing document indexes on a full
upsert. Opened 23 September.
34ec8503 2026-09-18 test:
reproduce ghost documents after cold load0e02275d 2026-09-22 test:
require cold load to reject unindexable stored documents41a27a59 2026-09-22 fix:
reject incomplete collection loads before publication79094cef 2026-09-22 test:
cover cold-load batch failure and recovery9d64307a 2026-09-22 fix:
rebuild missing document indexes on full upsert32a5faf7 2026-09-23 fix:
terminate safely on fatal collection load9e57bb86 2026-09-23 fix:
skip unindexable collections during restoreContinues the theme of section 7. The branch restores missing registrations on load, removes a registration together with its field and resolves the alias before comparing registrations. Opened 29 September.
8549e3d8 2026-09-28 fix:
remove async reference registration on field dropee2ef050 2026-09-28 fix:
repair stale reference registrations on load0b2259d8 2026-09-29 fix:
remove the exact dropped reference registration9d4613a1 2026-09-29 fix:
retain deferred alias references during loadb34606b2 2026-09-29 fix:
restore missing reference registrations on loadb610fb72 2026-09-29 fix:
compare reference registrations during alterde4d5104 2026-09-29 fix:
promote alias reference after field replacementThe same iterator code as section 4. The branch exposes numeric range postings for reuse, defers nested range filters until final materialisation and bounds that materialisation while preserving timeout cancellation. Opened 22 September.
46e5f4c5 2026-09-21 Reuse
numeric range postings in lazy filter iterators5e8cbef7 2026-09-21 Expose
numeric range postings for iterator reuse09eeaa5a 2026-09-22 Defer
nested numeric range filters until final materializationbe060bbd 2026-09-22 Bound
numeric filter materialization and preserve cancellation4616ad35 2026-09-23 Clarify
filter timeout and subtree predicate namessearch_cutoff and 408 for union searches (4 commits)The cutoff flag was reported from the last sub-search of a union rather than from all of them. The branch fixes that and answers 408 when a union is cut off with nothing to return. Opened 15 September.
b868535f 2026-09-02 fix:
report search_cutoff for every sub-search of a union, not the
last5b38dba2 2026-09-15 fix:
answer 408 when a union search is cut off with nothing to
return8c0f0a15 2026-09-17 test:
cover union search_cutoff with a pinned hit and over HTTP0edb3e5d 2026-09-22 test:
set up the union cutoff collections in a beforeAll hookThe same fix that reached v31 through #3058 on 20
September, described in section 8. Its branch was still open at the
cutoff.
3a1e0fd3 2026-09-01 fix:
skip remote model endpoint validation on collection load
(#2988)a6573628 2026-09-01 fix:
reject query embeddings whose size does not match the field
dims72691fad 2026-09-03 test:
bind the schema literal to an lvalue before create_collectionSupport for EmbeddingGemma models: the tokenizer, truncation before
copying pieces in GemmaTokenizer::Encode, and unit tests.
Opened 6 September.
9d3c5d79 2026-09-06 Truncate
before copying pieces in GemmaTokenizer::Encodec435c658 2026-09-06 Add
unit tests for GemmaTokenizerf423c9c0 2026-09-06 Add
a gemma tokenizer type for EmbeddingGemma modelsResumable rebuilds of maximum scores in the radix tree, and an API entry point to trigger them when the scores are stale. Opened 11 September.
95f62b9d 2026-09-11 feat(art):
add resumable max score rebuildse58ef903 2026-09-11 feat(api):
expose token score rebuildingefb46aa3 2026-09-11 feat(index):
rebuild stale token scoresThe version bump, a clear reason in the log when an HTTPS certificate or key is rejected, and listener tests with 2048-bit and 1024-bit keys. Opened 23 September.
81520812 2026-09-23 fix:
log OpenSSL’s reason when the HTTPS certificate or key is
rejectedba76a353 2026-09-23 build:
bump bundled OpenSSL from 3.0.5 to 3.5.8 LTSc8dbf68a 2026-09-23 test:
cover the HTTPS listener with 2048-bit and 1024-bit RSA
certificatesSupport for Qwen3-Embedding models: the tokenizer and a hard failure on a malformed merge. Opened 6 September.
7b0392d7 2026-09-06 Fail
fast on a malformed merge, and drop a pointless constaf6c78a8 2026-09-06 Add
a qwen tokenizer type for Qwen3-Embedding modelsThe oldest open branch in this set, opened 28 April 2025. In September it took one commit on durable async batching.
af4b9b09 2026-09-24 fix
durable async document batchingOpened 17 April 2026. One September commit: snapshot export layout and regression coverage.
a4c8475a 2026-09-24 fix
snapshot export layout and regression coveragefilter_curated_hits with group_by on
a missing field (1 commit)A test only, no behaviour change. Opened 12 September.
c980559a 2026-09-12 test:
cover filter_curated_hits with group_by on missing group fieldUnion resolved collections by coll_id rather than by
name. Opened 14 September.
35bb8779 2026-09-14 fix(union):
resolve collections by name, not by coll_idThe pending write counter was not decremented when a replicated entry failed to arrive. Opened 23 September.
9ea93435 2026-09-23 fix(raft):
release pending write when a replicated entry failsmax_completion_tokens for GPT-5 and later models (1
commit)Opened 24 September.
3d669f39 2026-09-24 fix(nl-search):
support gpt-5+ models with max_completion_tokensint32 -1 sentinel colliding with facet id 1 (1
commit)The same kind of collision as sections 1 and 6, here in a facet identifier. Opened 24 September.
a442504b 2026-09-24 fix(facet):
prevent int32 -1 facet_id sentinel collision with 1The benchmark workspace: explicit approval of the
esbuild and protobufjs build scripts. Opened
24 September.
b6ccee34 2026-09-24 fix(benchmark):
approve esbuild and protobufjs build scripts for pnpm 10+The memory guard rejected a write without first trying to return jemalloc’s dirty pages to the system. Opened 28 September.
b5c68399 2026-09-28 fix:
retry memory guard after purging jemalloc pagesThese commits were written in September, but on 30 September none of them had an open pull request, so they cannot be attached to an initiative above.
4683379a 2026-09-30 fix(search):
avoid recursive collection lock for curations53ebb424 2026-09-30 Fix
group_by merging two different composite group keys into
one groupb8363a38 2026-09-23 ci:
pin actions and scope workflow permissionsf0c6f9ea 2026-09-24 fix:
heap overflow when inflating highly compressible gzip bodiesThe first of them, b8363a38, also shows why the window
is taken by merge date rather than by commit date: it carries a
September committer date, but on 30 September it was not part of
v31.
Three September initiatives were closed without merging: #3053 (accent insensitive search, 11 commits on 16 and 17 September), #3081 (reference intersection logic) and #3089 (stale search results after deleting documents). The data collection does not fetch branches of pull requests closed without merging, so their content could not be read. None of them is merged work, so the month’s set of changes is unaffected, but the reason each was closed, and whether a successor exists, is not established here.
The figures in sections 4 and 12 are author reports. Reproducing either requires a build and is outside this review.
Everything in this review is bounded by the cutoff of 30 September. Later changes to any of it, including merges of the branches listed as work in progress, are not reflected here.
In section 4 the pull request description disagrees with the merged code on the ratio constant. The code value is reported, and the raw benchmark output behind the tables was not published, so the tables are reproduced as given.
Typesense is a trademark of Typesense, Inc.