Independent demo based on public GitHub data. PRA is not affiliated with or endorsed by Typesense, Inc.
Repository typesense/typesense, branch v31,
window [2026-10-01T00:00:00Z, 2026-11-01T00:00:00Z).
Changes were selected by merge date, taken from the committer date of
the commit in the v31 history and checked against the pull
request it closes. Author dates of branch commits were not used for
selection.
Tests were read in the diffs, not run. Where a pull request author or
a commit message reports a measurement, this review says so and names
the source. Code links are pinned to the merge revision, so behaviour on
current v31 may differ.
Before: a join lets a search read fields of another collection
through a reference, written as $authors(country:=US) in
filter_by and $authors(name,country) in
include_fields. An API key described which collections it
could act on and with which actions, and said nothing about joins. A key
restricted to books could therefore reach fields of any
collection that books referenced.
After: api_key_t gains a joins member. It
is nullptr when absent, which preserves unrestricted join
access for keys created before this change, and otherwise an object
mapping a collection name to allowed_fields and
denied_fields. An empty allowed_fields allows
all fields of that collection, an empty denied_fields
denies none, and denied_fields wins over
allowed_fields. A collection absent from the object cannot
be joined at all, and "joins": {} therefore prohibits every
join the request asks for or that natural language search generates.
Naming a field outside the policy returns 403. Wildcard and implicit
joined projections are intersected with the allowance rather than
rejected.
The policy travels in a reserved embedded parameter,
__typesense_join_policy.
AuthManager::authenticate sets it for
documents:search, documents:get and
documents:export. authenticate_parse_params
erases whatever a scoped key’s payload carried under that name before
setting it from the root key, so a scoped key cannot forge or broaden
the policy it inherits. validate_field_names=false does not
disable the check, and PATCH /keys/:id with
"joins": null is the one way to remove a policy, distinct
from omitting the field, which preserves it.
api_key_t::validate rejects a non-object
joins, an entry whose value is not an object, an unknown
property inside an entry, and a non-array or non-string field list, each
with its own message.
#3125 follows a day later and changes no externally visible behaviour. Join policy filter validation no longer resolves document ids, and the resolved policy is cached for the duration of one search instead of being rebuilt per joined field.
Code: 323df78,
a9d9b69,
include/auth_manager.h, include/field.h,
src/auth_manager.cpp,
src/collection_manager.cpp, src/core_api.cpp,
src/join.cpp.
Tests read:
AuthManagerTest.JoinPolicyIsPersistedAndInheritedByScopedKeys,
.ValidatesJoinPolicyShape,
.PatchKeyCanRemoveJoinPolicy,
CollectionJoinTest.APIKeyJoinFieldPolicy,
.ScopedAPIKeyJoinPolicyIntegrationTest,
.APIKeyJoinPolicyRejectsNestedFacets,
.APIKeyJoinPolicyPreservesWildcardFacetMetadata,
.APIKeyJoinPolicyAppliesAfterCurations,
CoreAPIUtilsTest.JoinPolicyAppliesToExportAndDocumentGet.
Limits: the pull request lists the cases it intends to cover and the
tests follow that list. This review read the diff and did not attempt an
independent search for a path that reaches a joined field without
passing through enforce_join_policy. Documents written
through a key are a separate path and are not covered here, see the work
in progress section.
Before: AuthManager::add_item_to_params appends a scoped
key’s embedded filter_by to the request’s own. When a
regular expression decided that both strings looked like a join,
Join::merge_join_conditions merged them instead, and the
comment in the code accepted that the expression had false positives.
Separately, nothing required the request’s filter to be a complete
expression, so an unclosed backtick in it could swallow the embedded
filter appended after it.
After: the merge path and the regular expression are gone. The
request’s filter is tokenized inside parentheses and rejected unless the
token stream terminates on its own, and the embedded filter is then
always joined with &&.
Pull request: #3111.
Code: b55704d,
src/auth_manager.cpp.
Tests read:
AuthManagerTest.AppendsEmbeddedJoinFilterWithoutMerging,
.RejectsUnterminatedFilterBeforeAppendingEmbeddedFilter,
CoreAPIUtilsTest.ScopedJoinFilterCannotBeBypassedWithOr,
.SearchCacheShouldRespectScopedEmbeddedFilters.
Limits: removing the merge changes behaviour for a request that joins
the same collection as its key. Such a request now carries two join
conditions combined with && rather than one merged
condition. The pull request describes itself as a refactor and does not
say which existing requests change shape.
Before: each collection is guarded by a
std::shared_mutex. That lock admits a reader while a writer
waits, so overlapping searches could keep a document write out of the
lock for as long as they kept arriving. Separately,
Store::delete_range took the store mutex exclusively, which
made it wait for any compact_all() in progress, and the
batched indexer calls delete_range after every write
request, so writes stopped for the length of a compaction.
After: a collection has a second lock, write_turnstile.
A document writer holds it from the moment it begins waiting for
mutex until it has it, and a search passes through it
before taking its first shared lock, so a search that starts while a
write is waiting queues behind that write. The lock order is
alter_mutex, then write_turnstile, then
mutex. Nested shared locks taken during a join or a
reference do not pass the turnstile, because one that waited for a
writer while its thread already held another collection’s lock could
deadlock against a join in the other direction.
The hold is bounded and configurable.
write-priority-max-percentage takes an integer from 0 to
100 and defaults to 25. It can be set on the command line, in the
[server] section of the ini file, through
TYPESENSE_WRITE_PRIORITY_MAX_PERCENTAGE, or at runtime
through update_config. A writer holds the turnstile for the
setting multiplied by 20 ms, clamped to between 500 ms and 2 s, which is
500 ms at the default. After a hold of length T no writer may close the
turnstile again for T * (100 - p) / p, three times T at the
default, so back to back writes cannot gate searches continuously. A
writer that still has not taken mutex when its hold expires
logs a warning, at most once a minute per collection, and goes on
waiting with the turnstile open. Setting the value to 0 removes the
turnstile entirely and restores the previous behaviour.
Store::delete_range now takes the store mutex shared.
The commit states the reasoning: the mutex guards replacement of the
database handle, and RocksDB is itself thread safe.
Code: 5def5b4,
7567c57,
264111e,
ccebdb0,
c3fec78,
6e411f2,
c2f90d0,
f15f778,
include/collection.h, include/tsconfig.h,
src/collection.cpp, src/tsconfig.cpp,
src/store.cpp.
Tests read:
CollectionSpecificMoreTest.SearchStartedWhileImportWaitsRunsAfterTheImport,
.SearchStartedWhileDeleteWaitsRunsAfterTheDelete,
.SearchStartedWhileDeleteByFilterWaitsRunsAfterTheDelete,
.JoinSearchIsNotHeldBackByAWriteWaitingOnTheJoinedCollection,
.WriteStarvedByUngatedReadersHoldsBackSearchesOnlyBriefly,
.WriteAfterAWriterGaveUpDoesNotHoldSearchesBackAgain,
.AfterAWriteHoldsTheGateItStaysOpenLongerThanItWasClosed,
.AWriteAtHalfPriorityHoldsSearchesBackForASecond,
.ALowWritePriorityStillHoldsSearchesBackForHalfASecond,
.SearchStartedWhileImportWaitsRunsFirstWhenWritePriorityIsOff,
and the configuration cases in tsconfig_test.cpp.
Limits: the tests assert ordering and timing with waits inside one
process. The sources checked hold no measurement of search latency or
write throughput on a loaded server, and nothing explains how the
default of 25 was chosen. The delete_range commit gives no
measurement of the stall it removes.
Before: cascade deletion locked the parent collection exclusively and
then recursively locked every referencing collection exclusively,
holding each one as it went. A search held its own collection shared for
the whole index search and took a referenced collection shared during
join filtering. The two orders form a cycle, and neither a search cutoff
nor an HTTP timeout can break it, because both threads are blocked
inside shared_mutex acquisition rather than inside any
Typesense loop. Issue #3086
reports it.
After: the cascade tree is flattened into a list of deferred locks.
The deleter blocks on one lock while holding none of the others, then
takes the rest with try_lock. If any of them is contended
it releases the whole set and blocks on the contended lock in the next
round. A join that already holds that collection can then take one of
the released locks and finish.
Pull request: #3091.
Code: 4f8f4a2,
include/collection_manager.h,
src/collection_manager.cpp.
Tests read:
CollectionJoinTest.CascadeDeleteDoesNotHoldParentWhileWaitingForReferencingCollection,
and the existing .CascadeDeletion and
.CascadeDeleteOption were adjusted. The new test drives the
race through two hooks compiled only into the test build,
collection_manager_after_cascade_first_lock and
collection_manager_after_cascade_retry_release.
Limits: releasing and retrying removes the cycle and admits repeated rounds. Nothing in the diff bounds how many rounds a delete may take under continuous join traffic, and the sources checked contain no measurement of cascade delete latency under load.
Before: the housekeeper logs long running queries. It read each
in-flight request’s req->params map directly while the
search thread was free to mutate it, and multi_search
resets that map for every search it runs. Under enough load the
housekeeper crashed inside _Rb_tree_increment. The crash
handler then tried to dump the queries in flight, which meant taking
ifq_mutex, already held by the thread that had just
crashed. The process stopped there with the mutex held: every search
blocked in add_req(), the Raft loop stalled, and
/health went on answering ok.
After: add_req() builds the query log line on the
request’s own thread and caps the body at 16 KB, and the housekeeper
logs that snapshot rather than reading the live map. The crash handler
skips the dump when the crashing thread holds ifq_mutex and
waits at most one second for it otherwise. A 60 second
SIGALRM watchdog is armed inside the signal handler, so a
crash handler that hangs anyway can no longer keep a wedged process
alive.
Code: 86a3337.
Tests read: none. The commit adds no test, and it is a direct commit with no pull request, so there is no review discussion to read either.
Limits: the three parts address the crash, the deadlock and the hang separately. Nothing in the sources checked shows how often the original crash occurred or on what load.
Before: ReplicationState::handle_gzip sized its output
buffer at ten times the compressed chunk. Once that buffer filled, the
loop called inflate() again with next_out
already at the end of the allocation and avail_out reset to
the full buffer size, so zlib wrote past it. Any gzip encoded write body
compressing better than about ten to one, which a repetitive JSONL
import reaches easily, could corrupt the heap inside
inflate() or lose documents silently.
After: inflation goes through a fixed 64 KiB stack buffer and appends
to the output, so the output size no longer depends on the compressed
size. The loop uses Z_NO_FLUSH, since one gzip stream can
span several request chunks, and a stream still incomplete after the
last chunk returns Incomplete gzip stream. instead of
passing on a truncated body. A null zs.msg is tolerated and
zstream_initialized is reset after
inflateEnd().
This change is the September commit f0c6f9ea, written on
24 September, which had no pull request at the end of that month and was
listed there among the branches with no open pull request. It was opened
as #3117 on 14 October and merged on 28 October.
Pull request: #3117.
Code: f1cfbab,
src/raft_server.cpp.
Tests read:
RaftServerTest.DecompressesHighlyCompressibleGzipAcrossChunks,
which compresses 1 MiB of a repeated byte and feeds it in two chunks,
plus the added truncated stream and empty last chunk cases.
Limits: the fix is in the HTTP write path that Raft replication shares. This review did not check for other callers that size a buffer from a compressed length.
Before: hostname2ipstr returned the original hostname
when getaddrinfo failed, and
resolve_node_hosts skipped a node it could not resolve and
returned whatever it had. A partial peer list could therefore be
applied, which removes valid peers from the cluster, or a literal
hostname could be parsed as an address. A starting node gave up after
three parse attempts and returned an error.
After: a failed resolution returns an empty string, and
resolve_node_hosts returns an empty configuration as soon
as any host fails, so a partial list is never applied. At startup, a
node that already has a Raft log or a completed snapshot starts from the
membership recorded there, because braft uses initial_conf
only when there is no persisted state, and the periodic peer refresh
applies the configuration once every host resolves. A node with no
persisted state waits in ten second rounds and re-reads its nodes file
between them, so a correction made while it waits is picked up.
refresh_nodes and reset_peers now refuse to
act on a configuration that does not parse or is empty. Snapshot
detection changed from counting directory entries to
has_completed_snapshot, which ignores the temp
directory of an unfinished snapshot that braft deletes on init.
The same commit closes a gap in the proxy endpoint.
POST /proxy and its server-sent-event variant called
APIAcl::is_allowed with the peer allowlist, and an empty
allowlist allowed every source address. Peer addresses are not known
until all nodes resolve, so under
proxy-allow-only-peer-src-ips the window in which nothing
had resolved was also a window in which nothing was restricted.
is_allowed takes a require_src_ip_match flag,
passed as that setting, and denies on an empty allowlist.
Pull request: #3103.
Code: 58e513b,
include/api_acl.h, include/raft_server.h,
src/core_api.cpp, src/raft_server.cpp,
src/typesense_server_utils.cpp.
Tests read:
APIAclTest.DeniesWhenAllowedSourceIpsEmptyAndMatchRequired,
.DisallowsWhenDisallowedCidrsEmptyWhenSrcNotAllowed, and
the additions to raft_server_test.cpp.
Limits: the startup and refresh paths are exercised through unit tests of their helpers. The sources checked contain no multi-node run with a failing resolver. An earlier pull request on the same subject, #3100, was closed without merging on 8 October, one day before this one merged, and the reason is not recorded in the data checked.
This section covers fifty-six commits: the direct commits of 23, 25 and 26 October, the test timing commit of 24 October, and the merge of #3097 on 5 October. Every measurement below is quoted from the commit message that reports it, made by its author and not reproduced here.
Before: the C++ suite ran in a single process, and tests wrote to
fixed paths under /tmp/typesense_test, so two test
processes at once overwrote each other’s data. The ONNX Runtime build
was capped at three parallel jobs by a constraint dating from an older
commit. Pull requests from forks get no repository variables, so each
one missed the default branch’s Bazel cache and compiled every
dependency, which the commit message puts at 67 to 87 minutes for a
first run. Each run downloaded about 5.8 GB of models again, because the
Bazel test temporary directory starts empty. The API tests ran their
groups one after another with fixed sleeps between steps.
After, on the build side: tests keep their state under
TEST_TMPDIR when Bazel sets it, and the suite runs in eight
parallel shards, where the commit message reports the slowest shard at
52 seconds against 316 seconds for the whole suite in one process on 32
x86 cores. Lifting the three job cap on ONNX Runtime takes a cold build
of the server from 1,062 seconds to 336 seconds on the same machine. The
default branch’s Bazel disk cache is shared with every pull request,
keyed on the dependency files, refreshed on every push to the default
branch, and a run that only reads the cache may start from the previous
week’s. ONNX Runtime’s submodules are pinned to commits so its build is
cacheable, and its source tree is keyed on its build file and patch.
Downloaded models are kept between runs, one directory per shard. A
build with nothing cached moves to a larger runner named by
COLD_RUNNER, and the probe that decides this is skipped
when no such runner is configured. A pull request within the repository
runs the suite once, through its branch’s push run, and push builds are
limited to version branches.
After, on the API tests: the single node, multi node and standalone groups run concurrently, each standalone file in its own process, and the server is built in its own job so the tests start sooner. Fixed sleeps gave way to conditions: waiting for the voted leader, for a restarted server to finish replaying its log, for analytics to drain, and polling multi node synchronisation every 50 ms with a failure when it never converges. The job is bounded in time and uploads server logs on failure.
Two changes concern what CI exposes rather than what it costs. #3097
adds permissions: contents: read, actions: read to the
workflows, pins every action to a commit with its version in a comment,
sets persist-credentials: false on checkout, and replaces
shell interpolation of github context values with
environment variables. Separately, the step that uploads server logs
after a failed API test now scrubs the provider API keys out of those
logs first and fails the run if a key is still present, with the upload
made conditional on the scrub having succeeded. The commit states the
reason plainly: the server logs request payloads, provider keys
included, and anyone who can read the repository can download its
artifacts.
The group also contains one commit that updates the model name used
by the conversation API test, one that tightens a nested join test’s
wait, and two commits, c511155004 and
90ee58bb09, that change no files.
Code: 0d0e440,
5b178f0,
538be73,
6ab2e9c,
fb73129,
d3eb505,
0fe452f,
8861a79,
.github/workflows/tests.yml,
.github/workflows/benchmark-testing.yml,
api_tests/, test/.
Tests read: this section is the test and build infrastructure itself. The assertions added are inside the API test harness.
Limits: the figures are the author’s and come from commit messages
rather than from a published benchmark run, and none of them states the
runner image or the cache state beyond what is quoted. A faster suite is
not evidence that the suite covers more. These commits went into
v31 directly, so there is no pull request review to
read.
Before: each sub-search of a union clears the thread-local
search_cutoff flag before it runs, and
do_union read that flag once at the end, so the value
belonged to whichever sub-search happened to run last. A sub-search that
exhausted the time budget went unreported, and one that found nothing
its filter admitted returned before reading the clock at all. The same
flag gates the 408 a union returns when the budget left it nothing, and
CollectionManager::do_union returned
result_op, which is always ok at that point, so the handler
never saw the timeout. A timed out union answered 200 with an empty
body. The handler’s 408 branch for a union had therefore never run and
neither marked the response final nor streamed it.
After: the flag is accumulated across sub-searches, the 408 is
returned from CollectionManager::do_union, and the
handler’s union branch marks the response final and streams it.
This pull request was open at the end of September with four commits and was listed in the September review as work in progress. It merged on 6 October.
Pull request: #3052.
Code: d780448,
include/http_data.h, src/collection.cpp,
src/collection_manager.cpp,
src/core_api.cpp.
Tests read: UnionTest.SearchCutoffCoversEverySubSearch
was changed to expect the 408,
CoreAPIUtilsTest.UnionSearchCutoffWithNothingToReturnIsRequestTimeout
is new, and three cases were added to the HTTP level suite in
api_tests/tests/union_search_cutoff.test.ts.
Limits: a cut off union now answers 408 where it previously answered 200 with an empty body, which is a deliberate change of response for an existing condition.
Before: NaturalLanguageSearchModelManager::init
validated every stored model with a live call to its provider when the
server started. A provider outage or a revoked key during a restart
dropped the model from the manager, after which PUT and
DELETE on it answered 404. Separately, the parameter that
bounds a completion was chosen by model name, and the newer OpenAI chat
models reject max_tokens and require
max_completion_tokens, so a configured GPT-5 or later model
failed. Issue #3067
reports that case.
After: init passes a skip_api_call flag
through validate_model into each namespace validator, the
same approach #3058 took for embedding fields in September. Every static
check still runs at init, including parsing a GCP service account key,
and a stored model that fails one is still skipped. Creation and update
still validate with a live call. For the parameter, every
openai/ model whose api_url is unset or
resolves to api.openai.com now gets
max_completion_tokens, because that host accepts it on
every chat model. Models reached through a custom api_url
and azure/ models keep the name based rule, widened to a
single digit o<digit> token at a word boundary and to
a digit of 5 or more directly after gpt-, so
gpt-6-luna moves while gpt-41-mini,
gpt-4o and EleutherAI/gpt-j-6b stay on
max_tokens. A fine-tune id is classified by its base model.
vllm/ sends max_tokens and
temperature in validation as well as in search, and a
configured temperature is no longer rejected by model
name.
#3073 was open at the end of September with one commit and was listed there as work in progress. It merged on 30 October, and #3153 is its follow-up on the same day.
Code: be4d9c5,
9ff8fb0,
include/natural_language_search_model.h,
include/natural_language_search_model_manager.h,
src/natural_language_search_model.cpp,
src/natural_language_search_model_manager.cpp.
Tests read:
NaturalLanguageSearchModelManagerTest.InitLoadsStoredModelsWithoutApiCall,
.InitSkipsStoredModelWithInvalidConfig,
.InitReloadsStoredModelsWithProviderParams,
NaturalLanguageSearchModelTest.OpenAIHostAlwaysUsesMaxCompletionTokens,
.OpenAICustomUrlKeepsMaxTokens,
.AzureGpt6RequestUsesMaxCompletionTokens,
.AzureProdO3MiniUsesMaxCompletionTokens,
.ReasoningModelNameRule, .OpenAIApiHostRule,
.VllmSearchSendsValidationParams.
Limits: the name based rule is a heuristic over model names, and the tests fix the classification for the names they list. A provider that changes which parameter it accepts, or a name outside those patterns, is not covered. No call to a live provider was made in preparing this review.
Before: the write guard rejects a write with 422
OUT_OF_MEMORY when measured memory is above its limit.
jemalloc can retain freed pages after a bulk delete, so measured memory
stayed high and writes kept failing although the documents were gone.
Issue #2926
reports it.
After: when the guard would reject a write, it asks jemalloc to purge
unused dirty pages and rechecks the operating system or cgroup reading.
The write is still rejected if memory remains high or the purge fails.
The purges themselves moved into housekeeping rather than running on the
request path, and the metrics endpoint gained allocator diagnostics:
typesense_jemalloc_dirty_pages,
typesense_jemalloc_page_size, the per-arena
dirty_decay_ms and muzzy_decay_ms, the
background thread run count and a read error counter.
The pull request reports that in an isolated 1.5 GiB container a 422 was confirmed under sustained pressure, and that after deleting 312,345 documents the next write returned 201. Those are the author’s figures.
This pull request was open at the end of September with one commit and was listed there as work in progress. It merged on 6 October.
Pull request: #3080.
Code: 60ae5e2,
include/cached_resource_stat.h,
include/housekeeper.h,
include/system_metrics.h,
src/cached_resource_stat.cpp,
src/housekeeper.cpp,
src/system_metrics.cpp.
Tests read:
SystemMetricsTest.ResidentMemoryIncludesActivePagesAndAllocatorOverhead,
excluded from AddressSanitizer builds, with the jemalloc read error
assertion restricted to Linux.
Limits: the container measurement is the author’s and was not reproduced. A purge costs time on the path that triggers it, and the sources checked do not measure that cost.
Before: Index::search sizes its result holder from the
larger of the group candidate limit, the fetch size, a default and the
number of pinned ids, then clamps that down to the approximate number of
documents the filter admits. The holder for curated results was
constructed with the same clamped size, so a filter_by
admitting fewer documents than there were pinned hits dropped the pins
beyond that count. Reported as issue #3120.
After: the curated holder is sized at the larger of that value and the number of pinned ids, so every requested pin has room. Pin filtering and grouping are unchanged.
Pull request: #3124.
Code: a4d3d35,
src/index.cpp.
Tests read:
CollectionCurationTest.PinnedHitsSurviveSmallFilterResults,
with one and two filter matches, grouped and ungrouped.
Limits: the fix enlarges one allocation per search that uses pinned hits. The pull request does not measure the memory effect of a very large pin list.
Before: nested object arrays are flattened into child fields for
indexing. Replacing or deleting such a field left the flattened child
values in the in-memory index, so after patching
{"reviewQueue": []} a filter like
reviewQueue.reviewerId:=9001 still matched the document
although the stored array was empty.
After: the update diff carries the full parent path through recursion
and uses the root .flat metadata and the root level
deletion output to remove flattened children missing from the update,
including arrays nested inside objects such as
wrapper.reviewQueue. Children supplied in the update go
through the existing diff logic, so changing one child while retaining
another keeps the document searchable by the retained one under both
update and upsert. Child scans are restricted to object parents and
object arrays.
Pull request: #2920.
Code: f66a7c1,
src/index.cpp.
Tests read:
CollectionNestedFieldsTest.UpdateNestedObjectArrayRemovesChildIndexes,
.UpdateDeepNestedObjectArrayRemovesChildIndexes,
.UpdateNestedObjectArrayPreservesUnchangedChildIndexes,
.IndependentDottedFieldSurvivesParentArrayReplacement,
.NestedArrayRemovalTracksMissingImmediately,
.NullObjectParentTracksMissingImmediately,
.FirstNullObjectArrayRemoval. The pull request states that
the two added regressions failed before the fix and passed after it.
Limits: the stale entries were in the in-memory index, and documents already written with them are corrected by a further update rather than retroactively. The pull request does not describe a repair path for an existing index.
Before: a curation rule can carry a filter_by applied
when its trigger matches the query. The filter pass re-matched a static
rule against the normalized query, and that match failed when the
request’s stopword set had removed a trigger token, so
shoes for men lost its filter under a stopword set
containing for. A stem: true rule compared
literal tokens rather than stem equivalent ones. A rule whose
replace_query had already rewritten the query could not
re-match it either. Separately, a dynamic filter rule with a mismatched
token count left the curation index out of step, so a later static rule
read another rule’s normalized query.
After: the request’s stopwords are stripped from the rule tokens
before the filter pass re-matches, stem rules match stem equivalent
query tokens on fields with and without stemming and remove only the
matched token, and a rule that rewrote the query applies its filter
without re-matching and keeps the replacement verbatim. The index desync
is fixed. Exclusion operators, phrase queries, field level
symbols_to_index and token_separators, and
chained stop_processing: false rules stay gated by the
filter pass as before.
This pull request was opened on 24 July and supersedes #2969, which was closed without merging on 16 October.
Pull request: #2999.
Code: 2224f51,
src/collection.cpp, src/index.cpp.
Tests read: fourteen cases in
collection_curation_test.cpp, among them
FilterByAppliedWhenTriggerQueryHasStopword,
ContainsRuleStemmedMatchAppliesFilterBy,
DictionaryStemRuleOnStemmingFieldMatchesRootForm,
ContainsRuleReplaceQueryKeepsReplacementVerbatim,
DynamicFilterRuleBeforeStaticRuleKeepsIndexInSync,
ExclusionOperatorDoesNotApplyFilterBy,
FieldLevelSymbolsGateFilterBy and
ChainedFilterRulesConsumeTokensInOrder.
Limits: a rule that previously did not apply its filter now does, which changes results for any deployment relying on the old behaviour. The pull request names the cases that must keep not applying the filter and tests them.
Before: query length was bounded only by request size.
After: --max-query-len sets a maximum number of Unicode
code points, with 0, the default, leaving the limit off. It can be set
on the command line, through an environment variable or in the ini file,
and it is server controlled, so a search parameter cannot raise it. The
query is truncated on a valid UTF-8 boundary before tokenization,
through the shared search path, so single, multi and union searches all
get it, and voice transcripts as well. The existing response cache
behaviour is preserved, with the truncated query as the key. Fixes issue
#2980.
Pull request: #3108.
Code: 2c81e74,
include/string_utils.h, include/tsconfig.h,
src/collection.cpp, src/string_utils.cpp,
src/tsconfig.cpp.
Tests read:
CollectionTest.MaxQueryLengthTruncatesBeforeSearch,
.MaxQueryLengthCannotBeOverriddenBySearchParameters,
.MaxQueryLengthTruncatesVoiceTranscriptBeforeSearch,
CoreAPIUtilsTest.MaxQueryLengthTruncatesSingleMultiAndUnionWithCache,
UnionTest.MaxQueryLengthTruncatesEachMember, and the
Unicode boundary cases in string_utils_test.cpp.
Limits: truncation is silent. The response carries no indication that the query was shortened, and the default leaves the behaviour unchanged for anyone who does not set the flag.
Before: Join::negate_left_join scanned every
non-matching child document and then built parent and child pairs from
them.
After: it uses the parent collection’s Index::seq_ids to
compute the negated result set directly.
The pull request reports search_time_ms on an inventory
dataset at five sizes. At one million documents a positive join took 45
ms, a negated join 54 ms, a negated join with no matches 33 ms and the
inner filter alone 30 ms. At thirteen million the same four were 511,
755, 505 and 429 ms. These are the author’s figures, and the pull
request gives no machine, build or concurrency conditions and no before
and after pair, so the table describes the new code rather than the size
of the improvement. Fixes issue #3131.
Pull request: #3133.
Code: b8c8a79,
src/join.cpp.
Tests read:
CollectionJoinTest.NegatedJoinsAndPreservesProductsWithoutReferences,
.NegateLeftJoinScrambledReferences, and
.FacetByReference was adjusted.
Before: Collection::update_metadata took a shared lock
and then assigned to metadata.
PATCH /collections/:name with a metadata body
therefore raced with every request that serializes the collection
summary, and the pull request reports a segmentation fault under
concurrent load.
After: the lock is exclusive. The change is one word.
Pull request: #3112.
Code: a2bb526,
src/collection.cpp.
Tests read: none. The diff adds no test.
Limits: the fix is correct for this writer. This review did not audit other writers for the same mistake.
Before: embedders are shared between fields through a key built from the model name, the project and the credentials, plus the number of dimensions. The region was not part of that key, so two fields configured for different GCP regions received the same embedder and therefore the same regional endpoint.
After: the key carries the region, with the default region used when a field does not name one, so a field without a region and a field naming the default region still share an embedder. This is part one of issue #3095.
Pull request: #3096.
Code: e9e9846,
src/text_embedder_remote.cpp.
Tests read:
CollectionManagerTest.GCPEmbedderIsKeyedByRegion, which
uses init_collection so that no network call is made.
Limits: the commit originates on the branch of #3104, which also changes how embedders are held and evicted and was still open at the end of the month. Only the keying part merged here.
Before: reference style $-prefixed names are added to
the resolved field lists before schema validation. With
validate_field_names=false an unresolved name still
returned 404 from query_by and remained in the resolved
group_by fields, so disabling validation did not have the
documented effect for those two parameters. Fixes issue #3135.
After: unresolved query fields are skipped and unresolved grouping fields are removed from the resolved vector, while field order, per-field query options and the errors raised under strict validation are preserved.
Pull request: #3141.
Code: 5d4da44,
src/collection.cpp.
Tests read:
CollectionSpecificMoreTest.IgnoreUnresolvedDollarQueryByFields,
CollectionGroupingTest.SkipMissingReferenceStyleGroupFields.
Before: brpc opens /proc/self/io while constructing its
global bvar objects. WSL does not provide that file, so
brpc called PLOG_ONCE(WARNING) before main()
had run, with glog not yet initialised, and the process crashed on
startup. Reported as issue #3090
against WSL 2.8 and later.
After: a patch applied to the vendored brpc includes
sys/uio.h and replaces that warning with
fprintf, which is safe before glog exists.
Pull request: #3093.
Code: b708744,
the brpc patch under bazel/.
Tests read: none. The change is a build-time patch to a dependency.
Limits: the fix is in a patch file carried by this repository, so it holds until the vendored brpc commit moves. The pull request reports no WSL run in the data checked.
Before: an object filter such as ingredients.{} produced
no filter node. toParseTree returned early on that path,
which left the nodes already on its stack unfreed, and errors from the
object filter parser were reported as 400 whatever code they
carried.
After: an empty object filter sets a 400 with the message
Object filter cannot be empty., the loop breaks rather than
returning, so the stack is unwound and freed, and a code raised by the
parser is passed through.
Code: 66fec87,
src/filter.cpp.
Tests read: FilterTest.ObjectFilterValidation, which
runs eight shapes of empty object filter alone and in conjunction,
disjunction and nesting.
Before: Collection::parse_facet walked the facet string
with an index of type int, and two of its inner loops were
written as for (i; facet_field.size(); i++). The condition
is the length of the string rather than a comparison against it, so it
is true for any non-empty facet string and the loop reads past the end
unless it meets a comma or a closing parenthesis first.
After: the index is a size_t and each loop tests
i < facet_field.size() alongside its delimiter checks.
The commit message calls this a clarity change. The reading past the end
of the string is a consequence of the same rewrite, and the commit adds
no test.
Code: ebfeb67,
src/collection.cpp.
Tests read: none.
Limits: whether the old loops could be reached with a facet string
that has no delimiter after sort_by or top_k
was not established here, so the practical effect is not claimed.
The commit moves the pinned commit of the project’s own hnswlib fork
from 687d9817 to 1c4befe8 and adds a comment
in Index::repair_hnsw_index. The comment records that
repair_m protects the index against being dropped but not
against resizeIndex(), and that a resize can invalidate the
level-0 pointers the repair uses once the index lock is released,
because mremap unmaps the old address when the buffer
moves. The commit message says the purpose is to avoid memory spikes
during index growth. That change lives in the hnswlib repository and is
not in this diff, so this review states the version move and not its
effect.
Code: cdf732d,
WORKSPACE, src/index.cpp.
Tests read: none.
Limits: the ingest’s diff for this commit contains only
src/index.cpp. The WORKSPACE line was read
from the commit on GitHub, and this gap is recorded in the internal
coverage note.
This lists the branches that took commits in October and were not
part of v31 on 31 October. Forty-three commits in all,
across twenty-seven pull requests that were open at the cutoff.
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 31 October 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.
Fourteen pull requests that were open at the end of September were still open at the end of October and took no October commits: #2320, #2878, #2993, #3038, #3039, #3041, #3043, #3045, #3063, #3064, #3066, #3075, #3076 and #3082. They are not listed below, because they have no October commits to list.
Ordered by the number of October commits.
Continues the theme of section 18. The branch keys GCP embedders by
region, the commit that reached v31 as #3096, evicts a
remote embedder when its last field is dropped, and refcounts embedders
per field so they survive between requests. Opened 8 October.
09b1fdd3 2026-10-05 fix(embedder):
key gcp embedders by region192da359 2026-10-05 fix(embedder):
evict remote embedders when their last field is droppeda9eedad7 2026-10-08 fix(embedder):
refcount embedders per field and hold them across requestsA multibyte character split across two request chunks. Opened 12 October.
7b29e249 2026-10-09 Add
UTF-8 request chunk regression41124f29 2026-10-09 Preserve
UTF-8 request chunks0ba64f42 2026-10-12 Avoid
serialization retries for split UTF-8dimensions sent to fixed-size embedding models (3
commits)Also in the embedder path. The branch stops sending a
dimensions parameter to models that do not accept one and
retries without it when a provider rejects it. Opened 16 October.
23ce87a0 2026-10-08 fix(embedder):
stop sending dimensions to openai ada-002 at request time1de995a9 2026-10-16 test:
cover openai ada-002 dimensions after a cold loadd7328234 2026-10-16 fix(embedder):
retry without dimensions when the provider rejects themHighlight end offsets on multibyte text, and prefix highlights around skipped symbols. Opened 28 October.
4be9a46d 2026-10-28 Fix
multibyte highlight end offsetse3503221 2026-10-30 Fix
prefix highlights around skipped symbols9de3df05 2026-10-30 Simplify
prefix highlight letter countingOpen since 15 September and described in the September review. In
October the branch undoes the store write when a record fails while
being indexed, and adds a test that pauses batch_index()
between the store write and indexing.
ec19ea78 2026-10-09 fix:
undo the store write when a batch record fails while being
indexed0f87918a 2026-10-12 test:
pause batch_index() between the store write and indexing to search
mid-importk, distance_threshold and hidden hits
applied to a brute force vector search. Opened 13 October.
cf35e911 2026-10-01 refactor:
split the grouped k growth out of the HNSW vector search78f90618 2026-10-01 fix:
apply k, distance_threshold and hidden hits to flat hybrid
searchDeclaration order when several dynamic field rules match, including after a schema alteration. Opened 14 October.
177f29e0 2026-10-14 Preserve
declaration order when matching dynamic fields9d6a72c4 2026-10-14 Preserve
surviving dynamic rule precedence during alterationOpened 16 October.
a1685dda 2026-10-16 Prevent
stalled import responses from blocking indexing23b4e643 2026-10-16 Harden
import response stall regression checksOpened 16 October.
9d4955da 2026-10-16 Preserve
no-hit analytics queries when expansion is enabled4709c294 2026-10-16 Avoid
redundant query copy in search analyticsContinues the theme of section 10. Search parameters generated by a model are not always strings. Opened 25 October.
a0ba7f00 2026-10-24 fix(nl-search):
accept non-string values in LLM-generated search params77433a03 2026-10-28 fix(nl-search):
skip array and object values in LLM-generated search paramsOpened 26 October.
b4b9ea28 2026-10-24 Preserve
per-field query tokens for mixed stemming78d93d77 2026-10-24 Preserve
exact mixed-field matches under candidate limitsContinues the theme of sections 1 and 2. A preset referenced by a scoped key is resolved before authorization, and the authorized collections become part of the search cache key. Opened 27 October.
47c26e50 2026-10-26 Resolve
scoped preset collections before authorization72bf7ded 2026-10-27 Include
authorized collections in search cache keysOpen since 23 September and described in the September review.
d803f7a4 2026-10-07 fix(raft):
respond with 503 when a replicated entry failsOpen since 24 September. It took fourteen commits in September and one in October.
587a38b2 2026-10-29 fix(nl-search):
cache the jev catalog for the schema prompt ttlgroup_by folding two keys into one group (1
commit)Opened 1 October around the September commit 53ebb424,
which the September review listed among the branches with no open pull
request at that cutoff.
afd471a3 2026-10-14 Add
a collection-level string[] regression test for the group id
foldOpened 8 October, on the same subject as the September branch #3082.
866050b5 2026-10-08 Fix
invalid stale async references on collection recreation.Related to section 2, where an unterminated filter is rejected before an embedded filter is appended. Opened 9 October.
af27c0d5 2026-10-09 fix(filter):
validate backtick pairing in filter stringsOpened 13 October.
195dd745 2026-10-01 fix:
compute the filter before the vector part of a hybrid searchOpened 14 October.
afa2e0f7 2026-10-14 Honor
field tokenization settings when highlightingOpened 14 October.
f0ca8cd3 2026-10-14 Normalize
custom stemming dictionary lookupsReports an authorization gap on a write path, which section 1 does not cover. Opened 21 October.
53ea7205 2026-10-21 Fix
single-document upsert API key authorizationOpened 21 October.
715129d3 2026-10-21 Preserve
synonym proximity window coverageOpened 23 October.
2a9b6d10 2026-10-23 Preserve
normalized Japanese query tokensvec_dist with num_dim of 0 (1 commit)Opened 25 October.
238db250 2026-10-25 fix:
validate vec_dist when num_dim is 0Opened 28 October.
56bb2268 2026-10-28 fix(analytics):
release per-user prefix query entries after compactioncuration_sets as a search parameter (1 commit)Opened 29 October.
72664b61 2026-10-29 Support
curation_sets as a search parameterOpened 29 October.
a47e453c 2026-10-29 Count
searches served from the search cache in search analyticsTen pull requests were closed without merging during October. Three of them were named as a gap in the September review, and the dates are now established: #3089 on 6 October, #3081 on 26 October and #3053 on 27 October. The other seven are #3092, #3100, #2532, #2984, #2969, #3143 and #3101.
Every measurement in sections 3, 8, 11 and 16 is reported by its author. Reproducing any of them requires a build and is outside this review. The table in section 16 has no stated conditions and no before and after pair.
Everything in this review is bounded by the cutoff of 31 October. Later changes to any of it, including merges of the branches listed as work in progress, are not reflected here.
Typesense is a trademark of Typesense, Inc.