All notable changes are listed here. The format follows Keep a Changelog and the project follows Semantic Versioning.
[Unreleased]
[0.4.0-preview.2] - 2026-11-03
- Fixed: Entity Framework Core inline apply skipped the first
SaveChanges. WithApplyInline = trueand noContextFactory(the configuration of the documented sample), the runtime had not yet learned how to create its own context when the firstSaveChangesfinished, so that save was not applied to the index. The error was kept only inOneIndexRuntime.LastApplyErrorand searches returned nothing for it. The rows were safely in the outbox. Anyone usingRecursiveAI.OneIndex.EntityFrameworkCore0.4.0-preview.1 that way should upgrade; after the upgrade the first apply (the nextSaveChanges, orOneIndexRuntime.For(db).CatchUp()) catches the index up from the outbox, so nothing needs re-entering. On 0.4.0-preview.1 itself, callingOneIndexRuntime.For(db)once (any search through the context does) and thenCatchUp()orReindexAsyncdoes the same. The context is now learned before the first change is saved. A configuration that can never apply (noContextFactoryand no public constructor taking the context'sDbContextOptions) now throwsInvalidOperationExceptionfromSaveChanges(withApplyInline) orRunAsync, before anything is written, instead of failing silently. - The
oneindextool's--versionprints the package version (for example0.4.0-preview.2), not the four-number assembly version. - Documentation: the install line for the tool needs
--prerelease; package sizes (.Models134 MB,.Models.Multilingual112 MB, the tool 151 MB); the README says the previews are on nuget.org; the EF and.Fileskill-campaign figures and the static index's first-query bytes (about 0.65 MB for the query, 0.74 MB with the manifest) agree across documents; internal markers removed from the synonyms and did-you-mean pages; the benchmark method page says where the published rows are.
[0.4.0-preview.1] - 2026-11-03
The first public preview.
Static index: term dictionaries and lengths are read in 4 KiB pages on first use and scattered files use 4 KiB chunks (
StaticBuildOptions.ScatteredChunkBytes,oneindex static build --scattered-kb), so a first query fetches about 0.63 MB instead of 2.6 MB. Static hybrid search takes its vector arm from IVF (StaticIndex<T>.HybridNprobe).IVF as a per-field vector index on the server (
[Vector(Index = VectorIndexKind.Ivf, IvfLists = n)]), stored in a new segment partivf.Query JSON: a ranking rule naming a field that cannot be sorted is a positioned parse error instead of an exception.
Multi-vector auto-encoding:
[MultiVector(From = ...)]encodes the source text at upsert through anIMultiVectorEncoder(SearchIndex.Open<T>(path, embedder, sparse, reranker, multiVectorEncoder)orSearchMultiVectorEncoders.Default), caches by model identity and text in stored source, re-encodes stale entries inReembedStale(), andSearchQuery<T>.MaxSim(...)is the late-interaction query stage besideRerank.IMultiVectorEncodergains batchEncode/EncodeAsyncwith default implementations.Windows: file handles allow sharing delete, rename replaces a destination that a reader has open, and readers use ordinary handles so cached lookups allocate nothing; the full test suite now runs on Windows in CI. The browser smoke app builds on its own CI job (it needs the wasm-tools workload) instead of with the solution.
Durability harness: the campaigns start their child processes correctly whether run through
dotnet run,dotnet Durability.dllor the application host.RecursiveAI.OneIndex.Analysis.Asian: Japanese and Korean lattice segmentation (IPADIC, mecab-ko-dic), Chinese most-probable-path segmentation (jieba word list) and Thai, Lao, Khmer and Myanmar maximal matching (ICU word lists), as the analyzersja,ko,zh,th,lo,kmandmy(AsianAnalyzers.Register(); core gainsAnalyzerRegistry.Register). Dictionaries are a checksummed container format (OIDICT1) with typed errors.Decompounding in core:
CompoundSplitter(Koehn and Knight splitting, fewest parts first),DecompoundFilter, packed word lists for de, nl, sv, da, no, fi and is, and the analyzers<code>-compoundand<code>-compound-both.RecursiveAI.OneIndex.Models: SentencePiece unigram tokenizer (SentencePieceModel,SentencePieceTokenizer) with the nmt_nfkc normalizer.New package
RecursiveAI.OneIndex.Models.Multilingual:MultilingualE5Embedder(multilingual-e5-small, int8 weights packed).More-like-this:
MoreLikeThisQuerypicks the most distinctive terms of a text or a stored document by term frequency times idf and searches them;index.MoreLikeThis(id),index.MoreLikeThisText(text)and the query JSON nodemoreLikeThis(docs/more-like-this.md). A missing source document raisesSourceDocumentNotFoundException.Facet value search on the happy path:
search.FacetValues(x => x.Category, "sho")counts over the documents of that search.Release builds fetch the packed model weights from a release asset checked against a pinned SHA-256 and fail without them (
tools/models/fetch-release-weights.sh).
[0.3.0-preview.1] - 2026-11-03
- Commit format 3: a flushed segment writes its text, column, point, stored and identifier parts into one pack file, so a small commit adds two files instead of eight (fewer flushes to disk per commit). Indexes written by earlier formats open, search and merge unchanged.
- Term proximity: user-text searches add a proximity bonus to the BM25F score (after Büttcher, Clarke and Lushman, SIGIR 2006), weight 0.1 by default; typed queries are unchanged.
- Ranking rules (opt-in): a bucket-sort order of
words,typo,proximity,attribute,sortandexactnessrules, declared with[RankingRules(...)], set per search with.RankingRules(...), or given asrankingRulesin query JSON. - Query JSON: a string holding an unpaired surrogate escape is a positioned parse error instead of an exception.
- Term dictionary format 2: nodes keep their labels, cumulative term counts and child distances in fixed-width tables, so an arc is found by label and fuzzy, typo and as-you-type expansion can look up only the arcs a Levenshtein state can still use. Fuzzy automata share precomputed shape transitions across queries. Version 1 segments stay readable and are rewritten as version 2 when merged.
- Block-max WAND re-sorts its clauses from the previous round's order instead of from scratch, which makes disjunctions of dozens of terms (fuzzy and typo expansions) about twice as fast.
- Merge policy:
TieredMergePolicy.SizeRatio(default 2) also merges the smallest segments when the largest of a group is at most that many times the others combined, so a settled index keeps a handful of segments and fuzzy, typo and prefix queries walk fewer term dictionaries; set it to 0 for the plain tiered policy. Bulk loads rewrite documents more often (about 3.8 times at 15 flushes against 1.7). - Filtered nearest-neighbour search: a filter now steers the HNSW walk (only passing documents are scored, with neighbours-of-neighbours expansion through rejected nodes, after the ACORN paper) and a selective filter switches to an exact scan of the passing vectors; both work on the compressed copies.
[0.1.0-preview.1] - 2026-11-01
Automatic embedding:
[Vector(From = ...)]embeds the source text at upsert through the index's embedder (SearchIndex.Open<T>(path, embedder)orSearchEmbedders.Default;BgeSmallEmbedder.UseAsDefault()in the models package), keeps the text and model identity digest beside the vector in stored source so reindexing, reopening, merges and schema rebuilds never embed unchanged text, andReembedStaleafter a model change.Reranking:
IReranker(a sibling ofIEmbedder),SearchQuery<T>.Rerank(...)as a stage over the best hits of any search (text, vector or hybrid) with the reranker's score and the first-stage score on each hit,Explainfor the stage,[FullText(Rerank = true)]to rerank plain text searches by itself, thererankproperty of query JSON, andMiniLmRerankerin the models package (cross-encoder/ms-marco-MiniLM-L6-v2, Apache-2.0) withWordPieceTokenizer.EncodePair.Hybrid search:
HybridQuery,SearchQuery<T>.Hybrid(...)and thehybridforms of query JSON fuse a text arm and a vector arm by reciprocal rank fusion (default) or weighted linear fusion; filters apply to both arms.Search(text)is hybrid automatically when the type has one auto-embedded vector member and the index has an embedder.Repository foundation: solution, shared build properties, packaging metadata, CI and release workflows.
Storage layer: a range-read interface with synchronous and asynchronous members, memory and local-file backends, a fault-injecting wrapper (torn, lost and reordered writes), and a block-transform slot.
Commit file format with a checksummed frame, and a crash-safe commit protocol, verified under seeded fault schedules.
Schema model with deterministic serialization, untyped documents, the analyzer pipeline contract with a reference analyzer, and the query, scorer, collector and segment-reader contracts.
Attribute types, the schema descriptor source generator, and shapes for
SearchIndexandSearchTool(not implemented).Dynamic JSON fields: a
[Dynamic]property is stored whole and its leaf paths are indexed by kind (keyword, number, bool, date), filterable by path from LINQ (Path), filter strings and query JSON, within limits on depth, paths and lengths.Schema evolution: opening an index with a changed schema rebuilds the segments that need it in the background from stored source, resumably, while the old schema keeps serving every segment; one commit makes the new schema visible on all of them; changes that cannot be converted fail at open naming the field. Commit format version 2 records the schema epoch and the rebuilt segments written ahead.