| title | Reference |
|---|---|
| nav_order | 13 |
| description | The packages, the architecture, the scoring conventions, the scope and the limits, and how to build and test LexiSharp. |
The contracts, the package layout, the conventions every engine shares, and how to build, test and reproduce the numbers.
| Package | Responsibilities |
|---|---|
LexiSharp |
records + interfaces + in-memory index + scorers + tokenizer + IEmbeddingProvider + ISparseEmbeddingProvider + sparse engine + in-memory dense engine + hashing embedding provider (export/import) + boost/rerank decorators + filters + highlighting + facets + similarity + keywords + metrics + hybrid federation (RRF, weighted, cascade, cross-encoder, MMR, MaxSim) |
LexiSharp.Postgres |
PostgreSQL providers: tsvector+unaccent (lexical), pgvector ANN (vector), pgvector sparsevec (sparse), pg_trgm+fuzzystrmatch (fuzzy), pg_search/Tantivy (true BM25) |
LexiSharp.MessagePack |
MessagePack (binary) persistence for the in-memory index and the sparse engine |
LexiSharp.AspNetCore |
a minimal-API GET /search endpoint over LexiSharpIndex<T> |
LexiSharp.Cli |
a .NET tool — lexisharp search, `segment build |
The four library packages target net10.0 and are on nuget.org.
LexiSharp.Cli is a tool rather than a library: it packs the core assembly inside itself, so
dotnet tool install -g LexiSharp.Cli has no other package to resolve.
The core package has no dependency; the others bring their own (MessagePack, Npgsql).
LexiSharp and LexiSharp.Postgres carry IsAotCompatible, so the trim/AOT analyzers
(IL2026, IL3050) are build errors. CI then publishes
tests/LexiSharp.AotSmoke
with PublishAot — ILC over the whole closure — and runs the binary: index, search, and a
document round trip through PostgreSQL. A regression fails the workflow on every push to main
and every pull request.
dotnet publish tests/LexiSharp.AotSmoke -c Release -r linux-x64 -o /tmp/aot-smoke
/tmp/aot-smoke/LexiSharp.AotSmokeWithout POSTGRES_TEST_CONNECTION the Postgres half reports itself skipped; ILC compiles it
either way, because the code is rooted whether or not the variable is set.
Not AOT-validated, and not claimed to be: LexiSharp.MessagePack and LexiSharp.AspNetCore.
MessagePack's resolver reaches for reflection on first use, so Save fails in an AOT binary
(MissingMethodException on ListFormatter<T>); the AspNetCore endpoint's
MapGet(route, Delegate) is annotated RequiresUnreferencedCode/RequiresDynamicCode and stops
the build.
Package Responsibilities
─────────────────────────────────────────────────────────────────────────────
LexiSharp records + interfaces + in-memory index + scorers + tokenizer + IEmbeddingProvider + ISparseEmbeddingProvider + sparse engine + in-memory dense engine + hashing embedding provider (export/import) + boost/rerank decorators + filters + highlighting + facets + similarity + keywords + metrics + hybrid federation (RRF, weighted, cascade, cross-encoder, MMR, MaxSim)
LexiSharp.Postgres PostgreSQL providers: tsvector+unaccent (lexical), pgvector ANN (vector), pgvector sparsevec (sparse), pg_trgm+fuzzystrmatch (fuzzy), pg_search/Tantivy (true BM25)
LexiSharp.MessagePack MessagePack (binary) persistence for the in-memory index and the sparse engine
Within the core package, separation of concerns mirrors the recommendations the library was designed from:
- the index owns corpus statistics (tf, df, document length, positions, vocabulary, and the
per-field statistics when a document declares
TextFields); - the scorer is a pure strategy reading from the index;
- the engine orchestrates query tokenization, scoring, filtering and ranking.
- A score of exactly
0means not a match and the document is excluded from results (all built-in scorers honor this). - One scorer departs from its published formula to keep that convention. Read literally,
BM25+ assigns
idf(t)·δto every query term, including terms the document does not contain, so the paper's model scores non-matching documents above zero. Reference implementations subtract a non-occurrence term to compensate. This implementation instead sums only over terms the document actually has, which keeps it anITermOverlapScorerand is the reason its scorer-level agreement with the paper is conditional. BM25L needs no such accommodation — its term weight is already0attf = 0. The arithmetic and the departure are in BM25 variants. - Every sub-system is culture-agnostic; text is normalized to lowercase without accents
so that
"Résumé"and"resume"match. - Scores are not comparable across engines unless you merge them by rank. A PostgreSQL
ts_rank_cd, a ParadeDB BM25 score, a BM25 score and a cosine are four different scales; RRF exists precisely because of that.
- Version 0.8.0, single maintainer. The library is young and its public API may still change between minor versions — pin a version and read the release notes if you adopt it early. Contributions and feedback are welcome.
- Built on established IR. The techniques implemented (BM25, RRF, SPLADE-style sparse retrieval, MaxSim) follow well-documented information-retrieval literature; the value here is a small, dependency-free .NET implementation of them, not new research.
- Performance numbers are indicative. They live in Benchmarks and were measured on one machine — always measure on your own corpus.
- Every "tuned" figure in this documentation is an in-sample upper bound. All four tuners fit and score on the same queries, so the more configurations were tried the more of the reported gain is fitting noise — 125 points fit more than 25. Score a winner on a held-out set before trusting its margin, and check that the objective is not flat: a tuner pointed at a flat metric has no signal to fit, and two figures in this repository's history were wrong for exactly that reason. See Tuning.
- No scorer here has a general measured win over a tuned BM25. Two of the three that could
plausibly have one came back null, and the third wins on one corpus and loses on another —
publishing that is the point. The learned-sparse row reads above BM25 on three corpora, and that
margin belongs to the external model: the library stores and scores vectors it is handed and
implements no part of SPLADE. See
Learned sparse.
- BM25+ and BM25L, tuned on their own
δ, tie a tuned BM25 on the reference corpus and NFCorpus, and edge it by 0.002–0.004 on SciFact — an in-sample margin, so an upper bound rather than a result. On ArguAna, at a fixedδ, they lose 0.040–0.046; tuned there, both pickδ = 0and read the tuned BM25's 0.363, so the gap closes by removing the bound rather than by finding a better one. See BM25 variants. - BM25F's tuner reports
WeightingHelped = falseon the full query set of every corpus it was fitted on, so the field weight is never what moves those margins (a held-out half reportstrueby at most 0.002; see Evaluation). Against BM25 tuned it wins on two corpora and loses on two: 0.397 against 0.363 on ArguAna, 0.668 against 0.664 on SciFact, 0.309 against 0.311 on NFCorpus, 0.8867 against 0.8978 (nDCG@5) on the reference corpus — with the title weight left at 1.0 in all four. Every one of those is fitted in-sample. See BM25F. - Proximity is neutral at quarter strength and negative at full strength. See Proximity.
- BM25+ and BM25L, tuned on their own
- Corpus-derived expansion is a measured loss where it has been measured (0.8469 against BM25's 0.8751 on the reference corpus) and is not a default for that reason.
- The learned and fitted channels have no measured value here.
FeedbackAwareTextSearchEngineand the contextual boost add a bonus whose worth depends on the corpus and on the history they are fed; no nDCG figure is claimed for either.CalibratedScoreConfidencefits a score → probability mapping and derives an abstention threshold from it, and no fitted J is reported for it on labelled data. For the feedback channel the open question is not only whether it pays but from how many recorded associations it starts to. Measure on your own data before relying on either — learning from past choices, fitted confidence. - Combinatorial coverage is partial. Engines, scorers, rerankers and mergers are tested individually and in the combinations described here, but not every pairing is exercised. Treat an unusual combination as supported but unproven until you test it on your data.
SearchTrace's time cost is unmeasured. Its allocation is pinned by BenchmarkDotNet and stable; its timing is not quoted because the baseline itself varied 2.3× between identical runs on the machine used. Read it as unknown, not as free — see Ranking trace.- In-box instruments and spans, no exporter.
RetrievalDiagnosticsimplementsIRetrievalMetricsover aSystem.Diagnostics.Metrics.MeternamedLexiSharpand exposes anActivitySourcenamedLexiSharp.Search, using types that are in-box on .NET 10 — the core package references no package for it. It ships no exporter, aggregator or dashboard, and no OpenTelemetry SDK dependency: a host's SDK, aMeterListeneror anActivityListenerattaches the backend. Engine and stage tags are caller-supplied and must stay low-cardinality; query text and document ids are not tags. See Observability.
dotnet build LexiSharp.slnx
dotnet test tests/LexiSharp.Tests # xUnit suite (Postgres tests need POSTGRES_TEST_CONNECTION)
dotnet run --project bench/LexiSharp.Benchmarks # BenchmarkDotNet suite (published numbers: benchmarks.md)The API reference is generated from those assemblies and the XML documentation they ship, so it cannot describe a surface that does not exist. A new public type with no regenerated page fails the build, which is the moment a type is still cheap to document:
dotnet run --project bench/LexiSharp.ApiDocs -c Release -- write # regenerate docs/api.md
dotnet run --project bench/LexiSharp.ApiDocs -c Release -- check # what CI runs; exit 1 if staleIts README is at
bench/LexiSharp.ApiDocs
— an absolute link, because a relative one resolves on GitHub and 404s on the published site,
which serves this folder from /LexiSharp/. It is worth reading before editing a row: the
What it is column is the type's own XML summary, so an edit that disagrees with the source is
overwritten on the next run.
Postgres/ParadeDB integration tests run against whatever POSTGRES_TEST_CONNECTION points to.
paradedb/paradedb:pg16 packages pgvector, pg_trgm and pg_search, so it runs the whole
suite with nothing skipped — verified on a fresh database, where the pgvector image still skips
the ParadeDB lane because pg_search is absent. The sparse tests install the vector extension
they need when the image packages it, rather than skipping a fresh database that has not had it
created yet; a server without pgvector still skips them, as does a server without pg_trgm
(and fuzzystrmatch, when exercised) for the fuzzy tests.
MIT — see LICENSE. The ParadeDB
pg_search extension used by the BM25 backend is licensed separately, under AGPL-3.