Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Full-Text Search

Full-text search finds documents by relevance, not exact substring — tokenized, ranked matching over one or more string properties. The library exposes a single FullTextSearch API that runs on each provider’s native full-text engine: FTS5 on SQLite, a generated tsvector + GIN index on PostgreSQL, a FULLTEXT index on MySQL, Oracle Text (CTXSYS.CONTEXT), a Full-Text Index on SQL Server, the fts extension on DuckDB, full-text search on Cosmos DB, and a $text index on MongoDB. LiteDB and IndexedDB have no native engine, so they fall back to an in-memory TF-IDF scan — the API works everywhere.

The shape mirrors vector search: register the searchable property/properties with MapFullTextProperty<T>(...), then query with store.FullTextSearch<T>("some text"). Results come back ordered by relevance descending, each with a Score.

Full-text is declarative and up-front: the index is created for you when the store initializes the type, so a type must be mapped before it can be searched — there is no ad-hoc full-text over an arbitrary property (unlike .Where(x => x.Body.Contains(...)), which works on any field). This is what lets the library create the underlying FTS index, generated/computed column, or $text index on your behalf — you never write the DDL.

services.AddDocumentStore(opts =>
{
opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=app.db");
// single property
opts.MapFullTextProperty<Article>(a => a.Body);
// …or several fields combined into one index
opts.MapFullTextProperty<Article>([a => a.Title, a => a.Body]);
});

It indexes the text value extracted at the property’s JSON path, not the raw JSON document. For a collection of strings or a custom projection, use the AOT-safe overload:

opts.MapFullTextProperty<Article>(
propertyNames: ["tags"],
textSelector: a => a.Tags, // IEnumerable<string>
language: FullTextLanguage.English);

Controls stemming / stop-words where the backend supports it (PostgreSQL regconfig, Oracle lexer, DuckDB stemmer, the in-memory fallback). Backends without per-language support ignore everything but the distinction between Simple (no stemming) and a stemmed language.

public enum FullTextLanguage
{
Simple, English, Spanish, French, German, Italian, Portuguese, Dutch, Russian
}

FullTextSearch is a terminal operation that returns up to maxResults documents ordered by relevance descending. Terms are OR-combined (a document matches if it contains any term); the exact operator grammar is provider-specific.

var hits = await store.FullTextSearch<Article>("orleans persistence", maxResults: 20);
foreach (var hit in hits)
Console.WriteLine($"{hit.Score:F3} {hit.Document.Title}");
public class FullTextResult<T> where T : class
{
public required T Document { get; init; }
public double Score { get; init; } // higher = more relevant
}

Score is normalized so higher always means a better match, but its absolute scale is provider-specific (BM25 on SQLite/DuckDB, ts_rank on PostgreSQL, MATCH … AGAINST on MySQL, textScore on MongoDB, the in-memory TF-IDF score, and a positional score on Cosmos, whose FullTextScore cannot be projected). Compare scores only within one result set, never across providers.

Pass a predicate to narrow results alongside the full-text match — for tenant or category scoping. It is pushed into the query where the provider supports it.

var techHits = await store.FullTextSearch<Article>(
"orleans",
filter: a => a.Category == "tech");

FullTextMatch on the query builder bridges to the same engine and folds the query’s current Where predicates into the pre-filter. OrderBy, GroupBy, and Paginate are ignored — maxResults controls the count and results come back ranked.

var hits = await store.Query<Article>()
.Where(a => a.Category == "tech")
.FullTextMatch("orleans", maxResults: 10);

FullTextSearch/FullTextMatch are terminal ranked calls. When you instead want full-text as a composable predicate — AND/OR’d with ordinary filters, paged, and sorted alongside everything else — use DocumentFunctions.LuceneMatch(field, luceneQuery) in a Where and DocumentFunctions.LuceneScore(field, luceneQuery) in an OrderBy/projection. They translate a Lucene query string to the provider’s native full-text engine over the same MapFullTextProperty index (so the type must still be mapped). The field argument identifies the mapping — pass a mapped property; the search spans the type’s whole combined index.

// filter + relevance sort + paging in one server-side query
var hits = await store.Query<Article>()
.Where(a => a.Category == "tech"
&& DocumentFunctions.LuceneMatch(a.Body, "orleans AND grain NOT deprecated"))
.OrderByDescending(a => DocumentFunctions.LuceneScore(a.Body, "orleans grain"))
.Skip(0).Take(20)
.ToList();

The same functions are available in the string-expression grammar (AOT-safe), lowering to the identical query:

store.Query<Article>().Where("lucenematch(body, 'title:quick AND brown~')");
store.Query<Article>().Where($"lucenematch(body, {userQuery})")
.OrderBy("lucenescore(body, 'orleans') desc");
FeatureExample
termorleans
phrase"virtual actor"
booleanorleans AND grain, a OR b, a NOT b, +must -mustnot (also && || !)
grouping(quick OR slow) AND fox
prefixdistrib*
fuzzyorlean~ / orlean~1
proximity"quick fox"~5
boostorleans^2 (affects ranking only)

Range queries ([a TO b]) and non-trailing wildcards (te*t) are rejected with a clear error.

Composable queries reuse the same per-type index as FullTextSearch, but each engine expresses a different subset of the grammar. Operators a backend can’t express throw NotSupportedException — they never silently degrade to something else.

ProviderLuceneMatchLuceneScoreAdvanced operators
SQLite (FTS5)✅ (bm25)prefix, proximity (NEAR)
PostgreSQL (tsquery)✅ (ts_rank)prefix
MySQL (BOOLEAN MODE)prefix, proximity
SQL Server (CONTAINS)✅ (CONTAINSTABLE)prefix, proximity (NEAR)
Oracle Text (CONTAINS)❌ — use FullTextSearchprefix, fuzzy, proximity
LiteDB / IndexedDB (in-memory)all (incl. fuzzy)
DuckDB / Cosmos DB / MongoDB❌ — use FullTextSearch

Baseline (terms, phrases, AND/OR/NOT, grouping) works on every provider in the ✅ rows. Notes:

  • OracleSCORE(n) requires a co-located CONTAINS label in the same statement, so a standalone LuceneScore can’t be produced; use store.FullTextSearch<Article>(...) for ranking on Oracle.
  • DuckDB / Cosmos / MongoDB — the engine can’t express the boolean Lucene grammar inline (DuckDB’s index is a rebuild-on-query snapshot; Cosmos/MongoDB full-text is bag-of-words / single top-level clause). Use the terminal store.FullTextSearch(...) / FullTextMatch(...) on those providers.
  • Field-scoped terms (title:foo) are not supported in v1 on any provider — the index is a single combined field — and throw. Pass the query without a field prefix; it searches all mapped properties.

The index is maintained automatically by the engine — no write-path bookkeeping — so once a type is mapped, Insert/Update/Remove/Clear keep it in sync for free.

ProviderEngineRankingNotes
SQLiteFTS5 virtual table + triggersBM25Always available.
PostgreSQLgenerated tsvector column + GINts_rankPer-language stemming.
MySQLgenerated column + FULLTEXT (InnoDB)MATCH … AGAINSTNatural-language mode.
OracleCTXSYS.CONTEXT (sync on commit)SCORE()Requires the Oracle Text option.
SQL ServerFull-Text Index + FREETEXTTABLERANKRequires the Full-Text Search feature.
DuckDBfts extension (BM25)BM25Snapshot index — rebuilt before each query.
Cosmos DBfull-text policy + FullTextScorerank orderNewer Cosmos service feature.
MongoDB$text indextextScoreOne text index per collection.
LiteDBin-memory TF-IDFTF-IDFNo native engine — scans the collection.
IndexedDBin-memory TF-IDFTF-IDFNo native engine — scans the store.
  • Map before you query. A type that hasn’t been mapped with MapFullTextProperty cannot be full-text searched — FullTextSearch/FullTextMatch throw. This is by design (it’s how the index gets created).
  • One mapped type per table/collection is the supported shape on engines that allow only a single full-text index per table (SQL Server, MongoDB).
  • Optional features must be installed. Oracle Text and SQL Server Full-Text Search are optional server components; if absent, FullTextSearch fails at query time. Cosmos full-text requires a service/SDK that supports it.
  • In-memory fallback (LiteDB, IndexedDB) loads and scans the collection, so it suits modest data sizes — exactly the client-tier scenarios those providers target.