Added
- Soft deletes with configurable retention period. When
IndexWriterConfig.SoftDeletesEnabledistrue,SoftDeleteDocuments(TermQuery)marks matching documents as deleted in the live-docs bitmap and records a Unix-millisecond timestamp in the.delfile. Soft-deleted documents are invisible to search but retained on disk until the retention period elapses, at which point merges reclaim the space. - Per-segment sequence number tracking. When
IndexWriterConfig.TrackSequenceNumbersistrue, each document is assigned a monotonically-increasing sequence number and the segment metadata recordsMinSequenceNumberandMaxSequenceNumber.IndexWriter.NextSequenceNumberexposes the next sequence number that will be assigned. UpdateDocuments(Query, LeanDocument)for atomically deleting documents matching a query and adding a replacement. SupportsTermQuery,BooleanQueryofTermQueryclauses, andMatchAllDocsQuery.IndexWriter.AddIndexes(MMapDirectory)to merge all segments from a source directory into the current index. Segments are validated for format compatibility and merged into a single new segment without modifying the source files.HunspellDictionarynow supports character ranges ([a-z],[A-Z],[0-9]) in affix conditions;AFalias directive parsing; morphological tag extraction from dictionary entries (word/flags po:verb); thread-safe content-hash-based dictionary caching across repeatedParsecalls; andFromFile/FromStreamconvenience overloads.StemTokenFilterwraps anyIStemmeras a composableITokenFilterfor use in theAnalyserpipeline.StemmerAnalyserprovides a generic analyser pipeline (tokenise → lowercase → stopwords → stem) accepting anyIStemmer, with factory methods for Porter, KStemmer, LightEnglish, and Hunspell backends.PorterStemmeris now a publicIStemmeradapter (previously internal viaPorterStemmerFilter.Stem).LightEnglishStemmernow has comprehensive unit tests covering irregular forms, plurals, past tense, progressive, derivational suffixes, protected words, and e-restoration.- Benchmark suites:
KStemmerParityBenchmarks,HunspellBenchmarks, andLightEnglishStemmerBenchmarks(accessible via--suite kstemmer,--suite hunspell,--suite lightenglish). - A Roslyn source generator (
Rowles.LeanCorpus.SourceGen) that turns[LeanDocument]-annotated models into typedLeanDocumentMap<T>s withToDocument,FromStoredDocument,CreateSchema, andFieldsdescriptors via direct, reflection-free, AOT-friendly code; ships attributes (LeanText,LeanString,LeanNumeric,LeanVector,LeanGeoPoint,LeanStored,LeanIgnore) and theMappingruntime surface (LeanDocumentMap<T>,LeanFieldBinding<T>,LeanField<T,V>,StoredDocument,LeanGeoLocation,LeanNumericEncoding,LeanNumericEncoders) in the core library, with diagnosticsLCGEN001–LCGEN013, strict-schema defaults, materialiser safety checks, stored round-tripping guidance, and a dedicated test project exercising generator output, diagnostics, nullability rules, encoder round-trips, and map round-trips. - Unicode-aware analysis components:
IcuAnalyser,IcuTokeniser,Uax29UrlEmailTokeniser,ThaiTokeniser,MediaWikiTokeniser,KeepWordFilter,TypeTokenFilter,LimitTokenCountFilter,FlattenGraphFilter,MetaphoneFilter,PhoneticAlternatesFilter,HunspellStemFilter,LightEnglishStemmer, and a lexicon-backedKStemmer. - Span-backed analysis APIs (
SpanToken,ISpanTokeniser,ISpanAnalyser,ISpanTokenSink,ISpanTokenFilter) for zero-allocation tokenisation;TokeniserimplementsISpanTokeniserandStandardAnalyserimplementsISpanAnalyser, feeding tokens directly toSpanPostingTokenSinkwithout allocatingList<Token>or per-token strings.Analyserconstructor acceptsISpanTokeniserdirectly; a staticAnalyser.FromTokeniser(ITokeniser, ...)factory wraps legacy tokenisers in a span adapter. BinaryFieldfor stored raw byte values, with typed stored-field codec support, binary doc-values mirroring, and binary retrieval throughSegmentReaderandIndexSearcher.- Index-time field boosting on document fields, persisted through norms, applied across text, boolean, range, vector, and geo scoring paths, and surfaced in score explanations.
- Payload-bearing term vectors and payload-preserving merge paths for postings and stored term vectors.
MatchAllDocsQuery,MatchNoDocsQuery,FieldExistsQuery,TermInSetQuery,PointInSetQuery,MultiPhraseQuery,IntervalsQuery, andCombinedFieldsQuery, with execution support inIndexSearcher, order-stable query-cache fingerprints, BKD exact-set lookup, and stored-only field-existence fallback.- Async indexing APIs on
IndexWriterfor single-document, batched, block, and commit workflows, using cancellation-aware backpressure waits while preserving the existing synchronous indexing core. - Streamed bulk ingestion from
IAsyncEnumerable<LeanDocument>with bounded batching. - A real
FstReaderover the FST1 blob format with arc-walk exact lookup, prefix enumeration, automaton intersection, and allocation-lightCollectIntersectOutputs/CollectOutputsWithPrefix/CollectContainsOutputsoverloads; extendedLevenshteinAutomatonwithMinDistance(state)so fuzzy callers can recover edit distance from the FST traversal. - Regexp query execution extracts a literal prefix from simple patterns like
gov.*mentormark.*and enumerates only the matching FST subtree, avoiding full field enumeration and the associated allocation explosion. A newIAutomaton.IsSinkdefault method lets the FST intersection path bail into a fast output-collection traversal when the automaton enters a fully-permissive state, improving wildcard throughput for patterns with interior*after early literal matches. - Offsets-only prefix enumeration for prefix and trailing-wildcard query execution when global document-frequency remapping is not needed.
- Span-sink n-gram benchmark variants to measure the allocation-aware tokenisation path separately from the legacy
List<Token>API. - Added these unit tests:
StringFieldandTextFieldnull-value guard,FieldType, andIsIndexedbranches.BooleanClauseequality: null object, wrong type, null typed, differing query, differing occur, equal instances, and consistent hash code.AggregationRequestnull-name and null-field guards, and forAggregationResult.Avgzero-count path andEmptyfactory.SegmentInfo.ReadFromwith a JSONnullliteral, verifyingInvalidDataExceptionis raised.InMemoryVectorSource:Countproperty,GetVectorhit,GetVectormiss (KeyNotFoundException), and null-dictionary guard.CompressionCodecRegistry.TryGetfalse path andGetunregistered-policy throw.GeoDistanceQueryequality: differ-by-field, differ-by-CentreLat, differ-by-CentreLon,Equals(null), andEquals(wrong-type).IndexInputEdgeCaseTestscovering EOF throws across all primitive readers (ref and non-ref), all five unrolledReadVarIntFastbyte-length paths, the fallback path, VarInt mid-decode EOF and overflow, corrupt UTF-8 sequences (3-byte, 4-byte, truncated, bytes-exhausted), heap-allocation path for charCount > 256 inReadUtf8StringandCompareCharsAndAdvance, andPrefetchon empty and non-empty files.IndexFormatInspectionOptions.IncludeChecksums,IndexFileInspectorcommit-file discovery and all error branches ofTryReadCommitandCheckCodecHeader, andVectorFilePaths.Sanitisecovering all character-substitution and heap-allocation branches.- FST reader allocation-light output-collector paths (
CollectOutputsWithPrefix,CollectIntersectOutputs,CollectContainsOutputs) and outputs-only enumeration (EnumerateOutputsWithPrefix,EnumerateContainsOutputs,IntersectAutomatonOutputs), covering prefix, wildcard, Levenshtein, IsSink fast-path, and field-qualifier overloads. - FST reader edge cases: corrupt-blob rejection (truncated header, wrong magic, out-of-bounds node address), large VarInt output round-trip (byte boundaries through to
long.MaxValue), final-output virtual arc (0xFF label for nodes that are both final and have child sub-keys), and deep-FST round-trip with 100 keys of ~1KB each. - FST builder edge cases: VarInt byte boundaries from 0 through
long.MaxValue, complex output distribution through nested prefix keys ("a"/"ab"/"abc"/"abd"), and frontier capacity growth with 100 keys of ~10KB each. - FST automaton edge cases: Levenshtein with maxEdits=5 against 220+ terms verified against brute-force, complex wildcard patterns (multiple
*/?mixed, Unicodecaf*), multi-byte UTF-8 prefix boundaries, andMinDistanceon dead/non-matching states. SegmentReaderpattern matching:GetFuzzyMatcheswith edit distance,GetTermsMatchingRegexwith compiled regex, andGetTermsInRangewith inclusive bounds.
- Added these integration tests:
SegmentReadermethods:GetFieldLength,GetDocIds,GetDocFreq,GetStoredFieldsnull path,GetNumericRangevariants, all DocValues readers, postings methods (GetPostingsEnumWithPositions,GetPositions,GetTermFrequency), pattern-matching methods (GetTermsMatching,IntersectAutomaton), and vector methods (GetVector,EnsureVectorReaderNoLock).IndexValidatorbranches: corrupt migration marker catch block, stale temp file patterns, segment-missing-files path,.fdt/.fdxmagic and version checks, doc count and block offset validation, missing deletion file, live-doc count mismatch, vector/HNSW magic, dimension, and normalisation checks, and deep vector/HNSW validation.IndexSearchermembers:Metricsproperty,SpanNearQuery/SpanOrQuery/SpanNotQuerycollection paths,BlockJoinQuerywith a non-TermQuerychild (exercises theBitArraypath inCollectChildDocsIntoBitArray), andVectorQueryexecution.SimdIntrinsicsVectorOpsAVX-512 paths (CosineAvx512,DotAvx512; conditionally skipped when unsupported) and three newIndexCodecMigratorpaths: non-executable plan, pre-migration validation failure, and auto-generated staging directory.IndexStats.TryLoadFromwith a JSONnullliteral returning null.SearcherManagerrefresh-failure paths:LastRefreshError,LastRefreshErrorAt,ConsecutiveRefreshFailures,RefreshFailedevent, subscriber-exception guard, and counter reset after recovery.- New query families: BM25F helper logic, multiphrase slot alternates, interval span semantics, set-query fingerprinting, BKD fallback on corrupt point trees, and explicit corruption failure paths for stored fields and positional queries.
- Binary fields, boost scoring, merge round-trips, and truncated payload and boost tails.
- Unicode-aware analysis components: extensible token types, MediaWiki token classes, phonetic alternates, Hunspell stemming, and token-budget guardrails.
- Async ingestion, source-failure retention semantics, backpressure cancellation, and async block indexing.
- Added these chaos tests:
- Corrupted
.pos,.dvn, and.veccodec files verifying structured exceptions are raised rather than silent data corruption. IndexCodecMigrator: read-only.dicforcing the exception catch path, and a staged migration where a corrupted.nrmtriggersValidateAfterMigrationfailure.IndexStats.WriteTo: read-only destination fires theUnauthorisedExceptioncatch block and cleans up the tmp file; file-locked destination fires theIOExceptioncatch block.
- Corrupted
Changed
- License changed to Apache 2.
- Breaking:
NGramTokeniserandEdgeNGramTokeniserno longer implementITokeniser; they only expose the zero-allocationISpanTokeniserpath and the stack-onlyEnumerateTokensenumerator. The legacyList<Token>-based methods have been removed. Whitespace scanning in split-aware paths (edge n-grams andSplitOnWhitespacen-grams) now happens inline instead of allocating a temporaryList<(int,int)>per call. TheMaterialisingTokenSinkis used internally byAnalyse()to produce theList<Token>output. NGram tokeniser benchmark suite has been pruned to the SpanSink, Streaming, and Lucene.Net comparison paths only. NGramTokenisernow accepts asplitOnWhitespaceconstructor parameter (defaultfalse). Whentrue, n-grams are generated per whitespace-delimited word rather than across the full input, eliminating cross-word-boundary grams and dramatically reducing allocations for larger gram ranges.- Removed the redundant pre-count pass from the
Tokenise(input, tokens)buffer overload of bothNGramTokeniserandEdgeNGramTokeniser; the reused list's existing capacity is sufficient after warmup and the O(n) scan is no longer performed on every call. The allocatingTokenise(input)overload retains its pre-count for correct initial sizing. - Added
LeanCorpus_NGramTokeniser_WordSplitbenchmark variant toNGramTokeniserBenchmarksto surface the per-word splitting path in benchmark runs. - The qualified-term interning and postings dictionary lookup are merged into a single alternate-lookup probe per token, eliminating the double hash computation.
FstBuilder.EnsureNodeCapacityletsTermDictionaryWriterpre-size the suffix-sharing registry to the unique term count, avoiding rehashing during FST construction. - Extracted
SegmentFlusheras a standalone static class, consolidating ~25 buffer collections fromIndexWriterinto a singleDocumentBufferStateclass.IndexWriter.SegmentFlush.cs(668 lines) is removed; all flush logic now lives inSegmentFlusher.Flush().SpanPostingTokenSinknow referencesDocumentBufferStatedirectly, eliminating theIndexWriterback-reference from the token sink. Zero allocation impact —DocumentBufferStateis allocated once in the constructor, same as the previous scattered initialisers. - Cut the on-disk term dictionary over to a real FST (Daciuk minimal acyclic transducer) with the new v3
.dicformat. Exact lookups become O(term length) arc walks with shared-prefix memory; prefix, wildcard, and fuzzy queries are now native FST × automaton intersections. v1 and v2 dictionaries are no longer opened by the live read path;TermDictionaryReader.Openthrows anInvalidDataExceptionwith a "run leancorpus-cli migrate" hint, andIndexCodecMigratorupgrades them in place via the legacy readers held underCodecs\TermDictionary\Legacy\. - Switched fuzzy matching to a byte-level (UTF-8) Levenshtein automaton. For ASCII queries this is identical to the previous char-level distance; for queries containing multi-byte code points the reported edit distance now counts UTF-8 byte edits rather than character edits.
- Renamed the real FST builder
FiniteStateTransducerBuildertoFstBuilderand moved the legacy v2 byte-array term dictionary (formerly misnamedFSTReader/FSTBuilder) and the v1 reader toCodecs\TermDictionary\Legacy\for migrator-only use. - Moved
kstem-dict.txtout of the embedded resources intolexicons/at the solution root.KStemmerno longer has a parameterless constructor; provide aKStemLexiconloaded viaKStemLexicon.FromFileorKStemLexicon.FromStream.KStemLexicon.DefaultandKStemLexicon.FromEmbeddedResourceare removed. - Moved the built-in Thai lexicon out of
ThaiTokeniserintolexicons/thai-dict.txt.ThaiTokeniserno longer has a parameterless constructor; provide a lexicon via the constructor,ThaiTokeniser.FromFile, orThaiTokeniser.FromStream. - Decoupled
IcuTokeniserandUax29UrlEmailTokeniserfromThaiTokeniser. Both now accept an optionalITokeniserfor Thai segmentation via their constructor. Without injection, Thai characters are treated as regular word characters. MediaWikiTokenisernow caches itsIcuTokeniserinstance as a field rather than allocating a new one per markup block.MediaWikiTokenisernow accepts an optionalUax29UrlEmailTokeniserparameter so the body-text tokeniser between markup blocks can be injected.- Replaced the enum-based
TokenKindanalysis contract with string token types, removed the public genericTokenTypestaxonomy, and moved producer-specific token type names onto the tokenisers that emit them. - Tightened new query constructors so empty fields, empty term groups, unknown combined-field weights, and non-finite point values fail fast instead of being silently filtered or coerced.
- Scoped lightweight phonetic and English stemming APIs to honest names, and added Hunspell condition parsing plus generated-form limits.
- Hardened async and batch indexing so schema validation runs before slot acquisition, dispose drains active indexing operations, block indexing suppresses mid-block threshold flushes until the parent marker is present, and partial indexing failures make the writer unusable until reopened.
- Precomputed
CombinedFieldsQueryunion document frequencies once per search execution and boundedTermInSetQueryterm counts. - Stopped mirroring stored
TextFieldvalues into binary DocValues by default, keeping binary DocValues forBinaryField,StoredField, and exactStringFieldvalues. - Changed norms boost storage to sparse entries so default field boosts do not write or load per-document
float[]arrays. - Changed phrase query execution to intersect candidate documents before decoding positional data for common multi-term phrases.
- Hardened benchmark suites by splitting block-join and deletion workloads, broadening Boolean and fuzzy query scenarios, aligning Lucene.NET disk-backed comparison paths, and making suite selection fail fast on unknown names.
IndexCodecMigratornow toleratesLLIDX033/LLIDX034validation errors caused by an outdated term dictionary on segments it is about to rewrite, so legacy.dicfiles no longer blockValidateBeforeMigration.
Fixed
NGramTokeniserandEdgeNGramTokeniserno longer hold a shared_wordOffsetslist; each span-path call and eachEnumeratorinstance now owns its own local list, making the span tokenisation path safe for concurrent use on a shared tokeniser instance.LowercaseFilterno longer holds a shared_spanBufferfield; the span path now rents a buffer fromArrayPool<char>.Sharedper call and returns it in afinallyblock, eliminating the shared mutable state.Analyser.Clone()added soCreateThreadLocalDocumentWritercan give each DWPT its ownFilteringSpanTokenSinkwhile sharing the (now stateless) tokeniser and filter references;IndexWriter.Concurrentswitches onAnalyserbefore the fallback arm that shared the original instance across threads.- Stored binary field reads now return defensive copies so callers cannot mutate cached stored-field buffers.
StoredFieldsReadernow validates matching.fdtand.fdxheader versions and block sizes before decoding stored values, and rejects unsupported.fdtversions up front.TermInSetQuerynow publishes its cached qualified-term array safely for parallel search execution.FuzzyQuerynow accumulates scores per document so multiple matching term expansions do not inflate hit counts with duplicate documents.- Hunspell affix parsing now rejects mismatched counted rule lines, applies cross-product suffix conditions to the prefix-modified form, and guards malformed strip lengths.
- Concurrent DWPT merges now preserve per-document binary DocValues and account merged postings for RAM-threshold flushes.