All notable changes to FluxIndex packages are documented here. Follows Keep a Changelog conventions.
WebFlux 핀을 0.5.3 → 0.6.0 으로 올렸다. WebFlux 가 범용 청킹을 FluxCurator 에 위임하면서
계약 위반 몇 건이 해소됐고, 그 결과 웹 콘텐츠의 청크 경계가 달라진다.
웹 인덱싱을 쓰는 소비자는 재인덱싱이 필요하다. 특히:
MaxChunkSize가 문서대로 토큰 수로 동작한다. 이전에는 문자 수로 강제됐으므로, 같은 설정에서 청크가 더 커진다(영어 문서는 대략 4배). 오차 크기가 언어마다 달랐으므로 변화 폭도 콘텐츠에 따라 다르다.ChunkOverlap이 실제로 적용된다. 이전에는 어떤 전략도 이 값을 읽지 않아 겹침이 0 이었다.Semantic전략은 임베더를 요구한다. 이전에는 조용히 문단 분할로 폴백했다.
파일 인덱싱 경로(FileFlux)는 영향 없다. 이 변경은 Integrations.WebFlux 에 한정된다.
전략 이름은 그대로이므로 ChunkingStrategyType 로 고르는 코드는 수정이 필요 없다.
상세는 WebFlux CHANGELOG.md 의 0.6.0 항목.
-
The keyword (sparse) leg has storage options of its own —
FluxIndexOptions.KeywordSearch(Provider,ConnectionString,UseVectorStoreConnection,EnableAutoMigration).It was the only storage component without them: registration lived inside the vector store's provider block, so the leg could only ever be placed wherever the vectors were. The split deployment this library itself recommends — vectors in Qdrant, metadata in PostgreSQL — therefore had no way to express a persistent keyword index at all, and consumers responded by copying the registration out of the library.
var builder = FluxIndexContext.CreateBuilder() .UseQdrant("localhost").AddQdrantStorage() .UseOpenAIEmbedding(apiKey); builder.Options.KeywordSearch.Provider = "PostgreSQL"; builder.Options.KeywordSearch.UseVectorStoreConnection = false; builder.Options.KeywordSearch.ConnectionString = metadataConnectionString; var context = builder.AddPostgreSQLStorage().Build(); // contributes the keyword leg only
The provider must be named. Left unset the leg follows the vector store, which is what the old gate did — and in this configuration that means Qdrant, so
AddPostgreSQLStorage()would contribute nothing. -
services.AddPostgreSQLKeywordSearch(connectionString, autoMigrate)— registers the leg directly on a service collection.IKeywordSearchServiceis resolved from consumers' own root containers by pipelines built on top of FluxIndex, not only from insideFluxIndexContext, and without a public entry point those consumers had to reproduce the singleton lifetime the indexer and the retriever depend on sharing. -
Naming a keyword provider whose package is not registered now throws at
Build(), with the call that is missing. Falling through to the in-memory index is the silent failure this option could otherwise introduce: hybrid search goes on returning vector-only results, so nothing reports the loss and the sparse leg is simply empty after every restart. Same guard the vector store and the cache already had.
Defaults reproduce the previous behavior, with one deliberate exception: asking for a connection of
the leg's own (UseVectorStoreConnection = false) and not supplying one now throws at registration.
It previously registered and failed later — or worse, indexed into whatever database the vector
store happened to use. An unset Provider means "follow the vector store", which is what the
vector-gated registration did. EnableAutoMigration is nullable on
purpose: the two backends did not agree before — PostgreSQL gated keyword provisioning on
VectorStore.EnableAutoMigration while SQLite always provisioned — so a non-nullable true would
have turned DDL back on for a caller who had switched it off, which is the one case that flag
exists to prevent.
Backfilled — this release shipped without a changelog entry.
-
The keyword leg takes metadata filters, in the same vocabulary as the vector store, so one filter object scopes both legs of a hybrid index:
KeywordSearchOptions.MetadataFilterandIKeywordSearchService.DeleteByFilterAsync, symmetric withIVectorStore.DeleteByFilterAsync.Filter dimensions are stored in a normalized side table rather than through JSON functions, so the predicate is an ordinary
EXISTS (…)on both backends — no per-dialect hook, and the backend-equivalence guarantee introduced in 0.23.0 stays intact. Existing rows can be backfilled without reindexing; the originalmetadatacolumn is retained for return values.
- A scope declared on a hybrid query reached the keyword leg.
HybridSearchServicedid not forward the filter to the keyword leg, and the SDK'sRetrieverdroppedMetadataFiltersentirely on the hybrid path while applying them on the vector-only path — so enabling hybrid search silently widened the scope. Qdrant's hybrid path filtered neither leg. Scope is now declared once per query and reaches both legs. DocumentIdFilterwas ignored by the in-memory store.EnablePhraseSearchwas ignored on the hybrid path.
-
Flux.Abstractionsis no longer built here. It now ships from its own repository and versions independently, starting at0.24.0. This repository consumes it as a package like any other consumer.The contract was previously produced here while also being consumed by FileFlux, FluxCurator, FluxImprover and WebFlux — all four of which this repository consumes in turn. That is a cycle in the dependency graph, and its practical effect was that those four could only ever reference a contract release older than the one they were being built against: raising the pin required a new release here, which immediately made the pin stale again. Freezing those pins was what kept the graph buildable.
Nothing about the contract's API changes — same types, same namespace, same package id, and the version line continues forward.
-
For consumers of this repository, nothing changes.
Flux.Abstractionsstill arrives transitively throughFluxIndex.Core. Only its origin and version line moved.
The keyword leg is now persistent on PostgreSQL as well, and the BM25 implementation is shared by every SQL backend so ranking cannot drift between them.
AddPostgreSQLStorage() registers PostgresKeywordSearchService, which keeps the BM25 inverted index
in the same database as the vectors. KeywordSearchAsync/HybridSearchAsync therefore keep working
after a restart and across processes, instead of degrading to vector-only (the degradation 0.21.5
started warning about). Schema provisioning follows VectorStore.EnableAutoMigration, like the vector
store's.
One reindex is needed to populate the keyword index for documents indexed earlier.
Verified against a live PostgreSQL (Testcontainers): 35 integration tests pass, covering the
restart roundtrip, ranking, deletion propagation, re-indexing without document-frequency drift,
document-scoped search, Korean whole-token matching, a 6,000-term batch through the array predicate,
and provisioning into a database that already holds other applications' tables. Six of them index one
corpus into both SQL backends and compare the results — order, scores, matched terms, term
frequencies and document lengths — so "ranking does not depend on the store" is asserted by execution
rather than inferred from the shared code. A nightly Integration Tests (PostgreSQL) workflow keeps
them running.
Scoring, tokenization, index maintenance and the index schema now live in
FluxIndex.Core.Application.Services.KeywordSearch.RelationalKeywordSearchService; each storage
package supplies only its SQL dialect (DDL, upsert syntax, id-list predicate). Consequences:
- Keyword scores are comparable between SQLite and PostgreSQL, and a fix in the shared code applies to both. A second hand-written copy of BM25 was the alternative, and it would have drifted silently.
SQLiteKeywordSearchServiceis unchanged in behavior and public shape; its SQLite schema DDL is byte-identical, so existing databases are untouched.- Removed
FluxIndex.Storage.SQLite.KeywordSearch.BM25TermEntity/BM25PostingEntity/BM25StatisticsEntity— unused EF entity types with noDbContextmapping (the service uses raw SQL). Breaking only for code that referenced the types themselves.
The option existed on the contract but no implementation read it: a caller scoping a keyword search to one document received global results. It is now applied to the postings themselves, not to the result set — filtering after the top-N cut would have returned nothing when the scoped document's matches sat below the global top N.
Recomputing document frequency inlined every affected term id into one statement. A batch touching
enough distinct terms would exceed SQLITE_MAX_SQL_LENGTH; the ids are now sent in bounded batches.
PostgreSQL passes them as one array parameter instead, so its statement size is independent of the
batch.
A null bound as a bare DBNull carries no type information and fails on providers that require one.
The only nullable column in the keyword schema is text (bm25_chunks.metadata), which is the common
case — most chunks have no metadata.
Two concurrency tests resolved one IVectorStore and used it from several tasks at once. The store is
DbContext-backed and EF Core forbids that; the corrupted connection state surfaced later as a
NullReferenceException while the service provider was disposed, failing roughly half of full-solution
runs. Each worker now resolves from its own scope. Test-only change — no product behavior is affected.
Fixture teardown also no longer calls the process-global SqliteConnection.ClearAllPools().
The hybrid keyword leg is now populated by indexing and persisted alongside the vectors — on the SQLite path. See "Not covered yet" below for PostgreSQL and Qdrant.
Indexer wrote to the vector store and nothing else. No indexing API touched the keyword (sparse)
index, so the hybrid keyword leg only ever held what the running process happened to search: empty
after a restart, and empty in any process that did not itself index. Hybrid search returned results
and looked fine while ranking by vector similarity alone (0.21.5 added the warning that made this
visible). Every mutation path now keeps the keyword index in step — IndexDocumentAsync,
AddChunksAsync, UpdateDocumentAsync, ReindexDocumentAsync, DeleteByDocumentIdAsync,
DeleteChunkAsync.
IndexerOptions.IndexKeyword = false stops the indexer adding to the keyword index. It is not a
compatibility switch: with nothing in the index, keyword search returns no results and hybrid search
ranks by vector similarity alone — whereas before 0.22.0 the keyword leg scanned chunk content and did
return something. Turn it off only if you do not use keyword or hybrid search. Deletions are still
propagated while a keyword index exists, so the option cannot leave postings for deleted documents
behind.
Consumers may need one reindex to build the keyword index for documents indexed before 0.22.0.
BM25 used the unsmoothed Robertson IDF log((N-df+0.5)/(df+0.5)), which is negative once a term
appears in more than half the documents. Combined with the default MinScore of 0, every such result
was discarded: the more common a term was in the corpus, the more certainly the keyword leg
contributed nothing. Now uses the smoothed form Lucene uses, log(1 + (N-df+0.5)/(df+0.5)), which is
always positive. Keyword recall improves (results that were being thrown away now appear) and
scores change. The default fusion method (RelativeScoreFusion) min-max normalises each leg before
applying the weights, and RRF is rank-based, so VectorWeight/SparseWeight keep their meaning on
those paths; the raw-score methods (Product, Maximum, HarmonicMean) do see an absolute-scale
shift in the sparse leg.
SQLiteKeywordSearchService read chunk content back from the vector store's private vectors table
instead of storing it, which made it unusable without a co-located SQLite vector store and, worse,
made DeleteByDocumentIdAsync a silent no-op whenever the vector rows had already been dropped —
the natural order when deleting a document, leaving keyword postings that still matched. It now owns
its payload (bm25_chunks). Re-indexing a chunk also replaces its postings instead of layering new
ones on top, so document frequency can no longer drift. Batch indexing commits once instead of once
per chunk.
Same defect class as 0.21.1 (PostgreSQL), still present for the SQLite vector store: the initializer
AddSQLiteStorage() registers called EnsureCreated(), which skips schema creation entirely if the
database holds any relation. Pointing FluxIndex at a database that already has your own tables meant
Build() succeeded and the first write failed with "no such table: vectors". Now provisions per
owned table, like every other component since 0.21.3.
ISparseRetrieverremoved.IKeywordSearchServiceis the single keyword contract; it is a superset (it also has the index-management and delete operations).BM25SparseRetrieverstill implements it, and its previously-explicitSearchAsync/GetStatisticsAsyncare now public.IHybridSearchServiceimplementations takeIKeywordSearchServiceinstead ofISparseRetriever. This is what lets a persistent backend serve the sparse leg at all.IDocumentRepository.SearchByKeywordAsyncremoved.Retriever.KeywordSearchAsync(unchanged as a public method) now reads the keyword index instead of scanning each document's chunks for a substring, so its results are BM25-ranked rather than substring-matched.QdrantHybridSearchServicetakesIKeywordSearchServiceinstead of the concreteBM25SparseRetriever, so a registered persistent backend reaches that path too.IndexerandRetrievertake an optional trailingIKeywordSearchService. Builder users are unaffected; callers constructing them by hand are not broken (the parameter is optional).
IndexerOptions.IndexKeyword(defaulttrue) andFluxIndexContextBuilder.WithIndexerOptions(...)— the builder previously had no way to configure the indexer at all, which would have left the new option unreachable.SQLiteKeywordSearchServiceis registered byAddSQLiteStorage()on the same database as the vector store, and its schema is provisioned duringBuild()like every other component.SQLiteKeywordSearchService.EnsureSchemaAsync().
The default in-memory BM25 index was registered Scoped, so each scope got its own empty index —
harmless while nothing wrote to it, a silent "no results" now that indexing does. Registered
Singleton, and with TryAdd so a storage package's persistent backend wins (storage registrations
run before the SDK's defaults, so a plain Add would have discarded them).
- PostgreSQL and Qdrant have no persistent keyword backend. On those paths the keyword leg is now correctly populated by indexing and benefits from the IDF fix, but it still lives in process memory and is empty after a restart. The PostgreSQL backend is the next piece of this work.
- CJK tokenisation is unchanged: the tokenizer splits on
\W+and Hangul is\w, so a Hangul run is never split —착수계does not match착수계약서. Whole-token queries work and are covered by tests.
SearchOptions has a HybridSearchOptions subclass carrying VectorWeight / KeywordWeight /
RerankingStrategy, and FluxIndexContext.HybridSearchV2Async honoured them — but
Retriever.SearchAsync built its Core options inline with hardcoded 0.7 / 0.3, so passing
HybridSearchOptions to the main search entry point changed nothing. Both paths now map through one
place (HybridSearchOptionsMapper); plain SearchOptions still gets 0.7/0.3, so default behaviour
is unchanged.
The keyword leg is process-local and no indexing API populates it, so after a restart — or in any process that did not itself index — hybrid search silently ranks by vector similarity alone. That limitation was documented in 0.19.0 but invisible at runtime: results came back and looked fine. Both hybrid paths now emit one warning when the keyword/sparse leg contributes nothing while the vector leg matched, naming the reason.
This is diagnostics only. Making the keyword leg survive a restart is the 0.22.0 work (the indexing API will populate the sparse index and persistent backends land with it).
AddPostgreSQLQuantizedVectorStore(...) registered the DbContext and the store but no provisioning
whatsoever — no initializer, no migration — so vectors and quantized_vectors were never created
and the first write failed even against an empty database. It now provisions through the same shared
routine as the other components, exposed both as an IStorageInitializer and as a hosted service.
The store is reachable only by direct registration, never from the SDK builder, which is why nothing
had surfaced it.
The PostgreSQL entity graph (EnsureEntityGraphSchemaAsync) and the SQLite vector, quantized and
main migration paths still created their schema with EnsureCreated, which does nothing once the
database holds any table — including tables another FluxIndex component put there. They now
provision per owned table like everything else.
The provisioner's existence probe issued a raw ADO command without enlisting the ambient EF
transaction, so provisioning from a context with a transaction in flight failed with "Execute
requires the command to have a transaction object". Caught by the SQLite native-extension
concurrency test. The probe now enlists CurrentTransaction when one is open.
Known remaining gap. SQLiteVecDbContext keeps its own bespoke initialization (vec0 virtual
tables plus a fingerprint-based re-init added in 0.20.2) and is deliberately left alone — its schema
is not fully EF-modelled, so the shared provisioner does not apply.
Upgrade note — when the partial-schema guard can fire. The components swept in 0.21.2–0.21.4 own
two or more tables each, so the "partially present" error is now reachable where it was not in
0.21.1. Upgrading alone cannot trigger it: no release has ever shipped one of these components with
fewer tables than it has today, so an older database is either complete or empty for a given
component. The realistic trigger is a name collision in a database shared with your own schema —
an existing cache_stats, chunk_relationships or similarly named table makes that component see a
partial schema and refuse to start. The message names the tables it found and the ones it wants; the
remedies are to give the index its own database or schema, rename the colliding table, or turn that
component's auto-migration off (EnableAutoMigration for the vector store, AutoMigrate for graph
and cache) and manage its schema yourself.
Fixed — the SQLite graph store, entity graph and semantic cache were never provisioned by the SDK builder
The SQLite side had the same defect 0.21.2 fixed for PostgreSQL, and it reaches further because SQLite
is the default local stack: UseSQLite(path) / UseLocalStorage(path) enable the vector store, the
graph store, the entity graph and the semantic cache, but only the vector store was provisioned by
Build(). The other three migrated from IHostedService implementations, which the builder never
starts. A freshly built database contained exactly one table — vectors — and the first graph,
GraphRAG or semantic-cache operation failed on a missing table.
Each component's migration now lives in one routine shared by both paths: an IStorageInitializer
the builder runs at Build(), wrapped by the existing hosted service for consumers registering the
stores directly. Provisioning creates only the tables each component owns (SQLiteSchemaProvisioner),
so components sharing one database file no longer suppress each other — EnsureCreated skipped
schema creation as soon as whichever component ran first had created anything, which is also why a
database shared with the consumer's own tables got nothing.
Regression coverage runs a real UseSQLite(...).AddSQLiteStorage().Build() and asserts each enabled
component's tables exist, including the derived entity-graph database file. It needs no container, so
unlike the PostgreSQL equivalent it runs in CI.
Known remaining gap. AddSQLiteVecVectorStore, AddSQLiteQuantizedVectorStore,
AddPostgreSQLEntityGraph and AddPostgreSQLQuantizedVectorStore are reachable only by direct
registration, not from the builder, and are still hosted-service-only (the PostgreSQL quantized store
has no provisioning at all). Tracked separately.
UsePostgreSQL(conn) enables the vector store, the graph store and the semantic cache on one
connection. The graph and cache schemas, however, were created by IHostedService migrations, and
FluxIndexContextBuilder.Build() never starts a host — it builds its own service provider and runs
the registered IStorageInitializer instances. So on the builder path those two components were
never provisioned on any database, fresh or shared, and the first graph or cache write failed
with 42P01. Only consumers who registered the stores directly into an application's service
collection (where the host runs the migration at start-up) were unaffected.
Each component's migration now lives in one routine that both paths share: an IStorageInitializer
the builder runs at Build(), wrapped by the existing hosted service for the direct-registration
path. Schema creation goes through the same owned-relation provisioner introduced in 0.21.1, so the
components no longer skip each other's tables when they share a database — which EnsureCreated
did as soon as any one of them had been provisioned first.
Also fixed: the vector store's provisioning is now reused rather than duplicated
(RelationalSchemaProvisioner).
Known remaining gap. The SQLite graph store, entity graph and semantic cache have the same
shape and are still hosted-service-only; UseSQLite(path) enables graph and cache the same way.
Tracked separately. PostgreSQL entity graph (AddPostgreSQLEntityGraph, not reachable from the
builder) and AddPostgreSQLQuantizedVectorStore (no provisioning at all) are also still open.
AddPostgreSQLStorage() provisioned the vector schema through EF's EnsureCreated(), which skips
schema creation entirely once the database contains any relation. Pointing FluxIndex at a
database that already held the consumer's application tables therefore created nothing: Build()
reported success and the first index write failed with 42P01: relation "vectors" does not exist.
Fresh databases were unaffected, so the failure appeared only in production.
The initializer now enumerates the relations its EF model owns, probes each with to_regclass, and
provisions through IRelationalDatabaseCreator when none are present — leaving unrelated relations
in the database untouched. The database itself is created when absent. A partial schema (some owned
relations present, some missing) is refused with an actionable exception instead of being silently
half-repaired; with the current single-relation model this guard cannot yet trigger, and it becomes
live as soon as the context owns more than one relation.
Reported by All.Manual. No API change — upgrading is enough.
Known adjacent gap (not fixed here). PostgreSQL graph, entity-graph, and semantic-cache still
initialize with EnsureCreatedAsync and, by default, on the vector store's connection. Tracked
separately; use AutoMigrate-off plus an externally managed schema until it lands.
Correction (0.21.2): that gap was worse than described here. Those components migrate from hosted services, and the SDK builder never starts a host — so on the builder path they were not merely skipped after
vectorsexisted, they never ran at all. Fixed in 0.21.2.
FluxIndex.SDK no longer depends on FileFlux, WebFlux, FluxCurator, or FluxImprover.
Each integration now ships as its own opt-in package:
| New package | Contains |
|---|---|
FluxIndex.Integrations.FileFlux |
FileFlux DI wiring + DocumentProcessingPipeline |
FluxIndex.Integrations.WebFlux |
WebFlux DI wiring + context builder extensions |
FluxIndex.Integrations.FluxCurator |
FluxCurator DI wiring + embedding adapters |
FluxIndex.Integrations.FluxImprover |
FluxImprover DI wiring + enrichment pipeline |
Why. The bundled graph made unrelated transitive vulnerabilities block FluxIndex CI:
0.19.0 failed restore on NU1902 (AngleSharp mXSS) reached through FluxIndex.SDK → WebFlux →
AngleSharp, in a library that does not use AngleSharp at all. Fixing WebFlux removed that symptom
but not the shape, so the next transitive advisory would have repeated it. Consumers now pay only
for the pipelines they use.
Migration. Add the packages you actually use and update namespaces:
<PackageReference Include="FluxIndex.SDK" Version="0.21.0" />
+ <PackageReference Include="FluxIndex.Integrations.FileFlux" Version="0.21.0" />- using FluxIndex.SDK.Extensions.FileFlux;
- using FluxIndex.SDK.Processing;
+ using FluxIndex.Integrations.FileFlux;
+ using FluxIndex.Integrations.FileFlux.Processing;Namespace mapping is mechanical — FluxIndex.SDK.Extensions.<X> → FluxIndex.Integrations.<X>
(same for .Adapters / .Services sub-namespaces), and FluxIndex.SDK.Processing →
FluxIndex.Integrations.FileFlux.Processing. No type names, signatures, or behavior changed.
Extension methods (AddFileFluxIntegration, AddDocumentProcessingPipeline*, AddWebFlux*, …)
keep their names.
samples/ChunkingQualityTestandsamples/FileFluxIndexSample— both referenced projects and packages that no longer exist (src/FluxIndex.Extensions.FileFlux, a bareFluxIndexpackage), were outside the solution so nothing built them, and had been untouched since 2025-11-29. README linked to both. Available in git history.
- SQLite-vec: writes silently broke after the effective embedding fingerprint drifted on a
latched store instance — once
SQLiteVecVectorStore.EnsureInitializedAsyncsucceeded it short-circuited on_initializedalone, so a later fingerprint change (e.g. aBindIdentityin another scope mutating the sharedSQLiteVecOptions) left subsequent writes targeting achunk_embeddings_{fingerprint}table that was never created (no such table). The store now tracks the table name captured at init and re-initializes when the current effective name diverges, creating the new vec0 table (CREATE VIRTUAL TABLE IF NOT EXISTS) before writing. Regression guard:SQLiteVecBindIdentityDriftTests.
SQLiteVecOptions.EmbeddingFingerprintdoc corrected: a null fingerprint throwsInvalidOperationExceptionfromGetVecTableName()— there is no automaticchunk_embeddings_{dimension}fallback (the comment contradicted the throw contract).
- EntityGraph (PostgreSQL):
EnsureCreatedfailed wheneverEmbeddingDimension > 0— the entity/communityEmbeddingcolumns were mapped as dimensionlessvector, which pgvector rejects for any vector index ("column does not have dimensions"). Columns now declarevector(EmbeddingDimension). Latent since the ivfflat era; exposed by the new schema integration tests. - EntityGraph vector indexes converted ivfflat → HNSW (entity + community), matching the main
vector store: ivfflat trains centroids at CREATE INDEX time, so an index created on an empty
table silently loses recall for data inserted afterwards.
EntityGraphOptions.IvfflatListsis now[Obsolete]and has no effect (removal in a future minor).
- Expired
NU1903(CVE-2025-6965) build suppression — SQLitePCLRaw 2.1.12 has shipped and src projects pin it directly; restore is warning-clean without it.
- Multi-value (MatchAny) metadata filters across every
IVectorStoreimplementation: a collection-valued filter entry (List<string>, arrays, JSON arrays) now matches when the chunk's metadata value equals ANY element — QdrantMatch.Keywords, PostgreSQL per-element jsonb@>OR-combined (each branch GIN-indexable), in-memory stores via the shared backstop. One query replaces the N-way per-value fan-out consumers previously had to run (filters: new() { ["document_id"] = fileHashes }). VectorStoreBase.ExpandFilterValue/VectorStoreBase.ValidateFilters— public helpers that define and enforce the filter-value contract for store implementations.- Shared filter-contract regression suite (
VectorStoreFilterContractSuite) run against InMemory, SQLite, and SQLite-quantized stores; PostgreSQL/Qdrant cover the same cases in their own suites.
- Unsupported filter values now throw
ArgumentExceptionat call time instead of silently matching nothing. Previously e.g. aList<string>filter value degraded to itsToString()type name and returned zero results with no signal; empty collections, nested collections, and arbitrary objects are rejected loudly. Validation is eager (atSearchAsynccall), not deferred to result enumeration.
PostgreSQLQuantizedVectorStore.SearchAsyncandSQLiteQuantizedVectorStore.SearchAsyncsilently ignored thefiltersparameter entirely, leaking chunks across filter scope (e.g. other tenants). Both now apply the shared match semantics before the topK trim.
FluxIndex.Extensions.FileVaultextracted to the FluxFeed repository. File-to-vector synchronization (git-like file tracking, folder monitoring, background ingestion) is now the FluxFeed document-pipeline surface (④b), which feeds into FluxIndex (④a). TheFluxIndex.Extensions.FileVaultpackage is no longer published from the FluxIndex family (family: 12 → 11 packages). The public API surface (IVault,AddFileVaultWithFluxIndex, etc.) is preserved, so consumer migration is a package-id + namespace swap (FluxIndex.Extensions.FileVault→FluxFeed), not an API rewrite. See docs/FILEVAULT_GUIDE.md for the migration note.
FluxIndex.Extensions.FileVault(MU-2): terminal-await for background memorize. The facade previously discarded the queued job id and returned an early-stageVaultEntry, so consumers in background mode polled entry stage / queue status to know when memorize actually finished ("success lie"). Two additive members:IVaultQueueService.WaitForJobAsync(jobId, ct)— signal-driven (no polling) wait that resolves on the Completed/Failed/Cancelled transition and immediately for an already-terminal job (race-free).IVault.MemorizeAsync(filePath, bool waitForCompletion, ct)— whentrue, awaits terminal completion and returns the entry at its Memorized stage; a failed/cancelled job surfaces as an exception rather than a silently-incomplete entry.falseis identical to the existing single-arg overload (zero regression). Reported via umbrella MU-2 (rule-of-three: AIMS, Filer, textree all hand-rolled completion polling).
FluxIndex.Extensions.FileVault: a removed entry could persist inListAsync(null)indefinitely after a preceding hybridSearchAsync. Root cause:VaultEntry.Load/SaveMetadataopenedmeta.jsonwithoutFileShare.Delete, so a concurrentListAsyncenumeration read blocked the background remove job'sDirectory.Delete(WindowsERROR_SHARING_VIOLATION), leaving the entry directory on disk (and growing it unboundedly). Now opened withFileShare.ReadWrite | FileShare.Delete, andVaultStorageService.DeleteEntryStorageAsyncretries the directory delete (5×, 100 ms backoff) to absorb the residualRemoveDirectoryrace and transient foreign locks. Entries stuck inRemovalPartialfrom before the fix self-heal viaRecoverPartialRemovalsAsyncon next host start. Reported by Filer (golden gateSC-RAG-1).
FluxIndex.Extensions.FileVault:VaultBackgroundService— replaced pollingTask.Delayloop with event-driven wake signal viaIVaultQueueService.JobEnqueued. Job scheduling latency drops from 5–10s (idle poll interval) to < 1ms after enqueue.
FluxIndex.Extensions.FileVault.Tests: Added[Trait("Category", "Integration")]toFileVaultPipelineSimulationTestsandVaultSubfolderScenariosTests— these were missing the trait despite living in theIntegration/folder, causing them to run with unit tests.
FluxIndex.Providers.OpenAI:WellKnownOpenAIModels— static lookup for 30+ model embedding dimensionsFluxIndex.Providers.OpenAI:OpenAICompatibleEmbeddingService(endpoint, apiKey, model, logger)constructor — auto-resolves dimension for well-known modelsFluxIndex.Providers.OpenAI:AddOpenAICompatibleEmbedding(endpoint, apiKey, model)DI overload — no dimension required for well-known models
build-and-release.yml: Pack step now usesdotnet pack FluxIndex.slnx(solution-wide) instead of per-project loop — ensures all family packages are published together at the same versionbuild-and-release.yml:version_checkstep now uses correctfluxindex.sdkpackage ID (wasfluxindexwhich doesn't exist on NuGet)
- Added
CHANGELOG.md(historical breaking changes from 0.2.x → 0.13.x) - Added
docs/MIGRATION.md(step-by-step upgrade guide for 0.2.x → 0.13.x consumers) - Updated
docs/AI_PROVIDER_INTEGRATION.mdwithFluxIndex.Providers.OpenAIofficial package usage - Updated
docs/README.mdwith MIGRATION.md quick link
FluxIndex.Core: RemoveTokenMeter.Abstractionsdependency —ITokenCounteris now defined locally
FluxIndex.Core: Remove unnecessaryFileFluxdependency
FluxIndex.Extensions.FileVault:IVault.RemoveAsync(IEnumerable<string>)batch overload
FluxIndex.Storage.SQLite: Clean legacy-fingerprint vec0 orphans on delete and startup sweepFluxIndex.Storage.SQLite: PassCancellationTokencorrectly toExecuteSqlRawAsync
FluxIndex.Core: All[LoggerMessage]strings translated from Korean to English (ASCII-only)- Added
LogLanguageConventionTestsregression test to prevent Korean log string regressions
ProcessingStage.Error— new terminal error stage for vault pipelineVaultStatus:RefinedCount,StaleCount,ErrorStageCountcountersIVault.GetErrorEntriesAsync— query entries in Error stage
EmbeddingIdentity/ModelFingerprintfor model-aware vector collection namingIVectorStoreManagerinterface with collection listing support- Require
EmbeddingFingerprintfor vec table naming (breaking for customIEmbeddingServiceimplementations that do not return a stable model name)
-
FluxIndex.SDK:AddOpenAIEmbedding(),AddAzureOpenAIEmbedding()extension methods removed. These were no-ops in prior versions. UseFluxIndex.Providers.OpenAIpackage instead:// Before (0.10.x, was already a no-op) services.AddOpenAIEmbedding(apiKey); // After (0.11.0+) // Install: dotnet add package FluxIndex.Providers.OpenAI services.AddOpenAICompatibleEmbedding( "https://api.openai.com/v1", apiKey, "text-embedding-3-small", dimension: 1536);
FluxIndex.SDK: Storage provider registration decoupled from SDK (was already separate but stubs removed)
- Dimension-aware vault + SQLite-vec table naming (breaking if using raw
IVectorStorewithout dimension)
FluxIndex.Providers.OpenAI— new package for OpenAI-compatible embedding and rerankingOpenAICompatibleEmbeddingService(endpoint, apiKey, model, dimension, logger)AddOpenAICompatibleEmbedding(endpoint, apiKey, model, dimension)DI extensionOpenAICompatibleRerankerService+AddOpenAICompatibleReranker(endpoint, apiKey, model)DI extension
FluxIndex.Providers.LMSupply— new package for LMSupply local embedding and reranking
- SQLite Entity Graph Store for local GraphRAG
- Unified storage provider architecture (auto-maximize)
Package consolidation — several packages were merged or renamed.
| Old package | Replacement |
|---|---|
FluxIndex.AI.OpenAI |
FluxIndex.Providers.OpenAI (added in 0.9.0) |
FluxIndex.AI.Anthropic |
Implement IEmbeddingService directly in your app |
FluxIndex.AI.Google |
Implement IEmbeddingService directly in your app |
FluxIndex.AI.Local |
Merged into FluxIndex.SDK |
FluxIndex.Extensions.FileFlux |
Merged into FluxIndex.SDK |
FluxIndex.Extensions.FluxCurator |
Merged into FluxIndex.SDK |
FluxIndex.Extensions.FluxImprover |
Merged into FluxIndex.SDK |
FluxIndex.Extensions.WebFlux |
Merged into FluxIndex.SDK |
| Old namespace | New namespace |
|---|---|
FluxIndex.Extensions.WebFlux |
FluxIndex.SDK.Extensions.WebFlux |
FluxIndex.Extensions.FileFlux |
FluxIndex.SDK.Extensions.FileFlux |
-
FluxIndexContextBuilder.UseOpenAI(apiKey, model)— removed -
FluxIndexContextBuilder.UseAzureOpenAI(endpoint, apiKey, model)— removed -
FluxIndexContextBuilder.UseLocalAI()— still available (ONNX local model)Migration:
// Before (0.2.x) var ctx = FluxIndexContext.CreateBuilder() .UseLocalStorage("index.db") .UseOpenAI(apiKey, "text-embedding-3-small") .Build(); // After (0.9.0+, using FluxIndex.Providers.OpenAI) var ctx = FluxIndexContext.CreateBuilder() .UseLocalStorage("index.db") .UseEmbeddingService(new OpenAICompatibleEmbeddingService( "https://api.openai.com/v1", apiKey, "text-embedding-3-small", 1536, logger)) .Build(); // Or with DI services.AddOpenAICompatibleEmbedding( "https://api.openai.com/v1", apiKey, "text-embedding-3-small", dimension: 1536);
-
Namespace reorganization:
FluxIndex.Domain.Entities→FluxIndex.Core.Domain.EntitiesMigration:
// Before (0.2.x) using FluxIndex.Domain.Entities; // After (0.3.x+) using FluxIndex.Core.Domain.Entities;
Affected types:
DocumentChunk,Document,SearchResultand all other domain entities.
Last version with:
FluxIndex.Domain.Entitiesnamespace (useFluxIndex.Core.Domain.Entitiesin 0.3.x+)FluxIndex.AI.OpenAIpackage (useFluxIndex.Providers.OpenAIin 0.9.x+)FluxIndex.Extensions.WebFluxseparate package (merged intoFluxIndex.SDKin 0.4.0)UseOpenAI()/UseAzureOpenAI()builder methods (removed in 0.4.0)
Initial public versions. Feature development.