TX Text Control .NET Server

RAG

Topic

A chat model knows what it learned during training, but not your travel policy, the payment terms you negotiated with a customer or the approved wording of a clause. Retrieval-augmented generation (RAG) closes that gap without retraining: the application finds the relevant passages in approved documents and sends them to the model together with the question, so that the answer rests on your material and can name its sources.

TX Text Control turns the documents into text that can be searched, and TXTextControl.AI.Knowledge of the TX Text Control AI preview stores, indexes and retrieves it. The code on this page was checked against version 0.1.0-beta.3; the API may still change before a stable release. How the preview packages fit together is described in Text Control AI, and questions about a single document in Chat with Documents.

How RAG Works

RAG has two phases:

  1. Index the references. Approved documents are uploaded, their text is extracted and split into bounded passages, and every passage keeps its source and version. Optionally, an embedding model computes vectors for semantic search.
  2. Retrieve for a question. The application searches the collection the user may access, selects passages within a token budget and sends them with the request to the chat model.

Uploading documents doesn't train the model, and its weights don't change. When a source is replaced, the next search finds the new text. The same question can therefore get a different answer with and without references, because the model receives a different context.

TXTextControl.AI.Knowledge

TXTextControl.AI.Knowledge is a .NET 8 library for private document retrieval. It stores its data in an embedded SQLite database with FTS5 full-text search and needs no search server, web host, chat model or document editor. It doesn't generate answers either: it returns evidence that your application passes to a chat model.

ConceptMeaning
CollectionA named set of reference documents, such as approved policies, with an owner
SourceA document in a collection. Uploading a file with the same name again creates a new version that replaces the old one once it's indexed
JobThe durable indexing task of an upload, with a state, a progress value and an error message
PassageA bounded part of a source that a search returns, with its file name, a locator and a score
Access contextThe authenticated user on whose behalf the store reads and writes

Install the prerelease package:

dotnet add package TXTextControl.AI.Knowledge --prerelease

The database is created on first use. The host needs permission to load the native SQLite library and to write to the database folder, including the WAL and SHM files next to the database. Use one store and one indexing worker per database, and don't share a database between several hosts.

Index and Search Documents

The default extractor of the store reads only TXT and Markdown files. The following console program therefore exports Documents/travel-policy.docx as Markdown with TX Text Control, indexes it and searches it:

using System.Text;
using TXTextControl;
using TXTextControl.AI.Knowledge;
using TXTextControl.Markdown;

// 1. Export the policy as Markdown
string markdown;

using (ServerTextControl tx = new ServerTextControl())
{
    tx.Create();
    tx.Load("Documents/travel-policy.docx", StreamType.WordprocessingML);
    markdown = tx.SaveMarkdown();
}

// 2. Open the store; in a web application, the subject comes from trusted authentication
using var knowledge = new KnowledgeStore(new KnowledgeOptions
{
    DatabasePath = Path.GetFullPath("private-data/knowledge.db")
});

var access = new KnowledgeAccessContext("authenticated-user-id");
var collection = await knowledge.CreateCollectionAsync("Approved policies", access);

// 3. Queue the document and process the queue
await knowledge.EnqueueAsync(collection.Id, "travel-policy.md", Encoding.UTF8.GetBytes(markdown), access);

while (await knowledge.ProcessNextAsync()) { }

foreach (KnowledgeJob job in await knowledge.GetJobsAsync(collection.Id, access))
{
    Console.WriteLine($"{job.FileName}: {job.State} {job.Error}");
}

// 4. Search the collection
var result = await knowledge.SearchAsync(new KnowledgeSearchRequest(collection.Id, "hotel costs"), access);

Console.WriteLine(result.Notice);

foreach (var passage in result.Passages)
{
    Console.WriteLine($"{passage.FileName} ({passage.Locator}): {passage.Text}");
}

The code works as follows:

  • The project references TXTextControl.TextControl.Core.SDK, TXTextControl.Markdown.Core and the prerelease TXTextControl.AI.Knowledge.
  • SaveMarkdown turns the headings, lists and tables of the DOCX file into Markdown. The extractor reads it as UTF-8 text, derives the locators of the passages from the headings and lines and repeats table headers.
  • DatabasePath must be an absolute path to a private folder, never one inside wwwroot.
  • KnowledgeAccessContext names the user. The default access policy grants access only to the owner of a collection. The fixed value suits a single-user console program; a web application takes it from its authentication and never from a value the browser sends.
  • EnqueueAsync returns a job ID. A queued document isn't searchable until its job is processed.
  • ProcessNextAsync processes at most one job. It returns false when there was no job to process, which doesn't mean that the jobs succeeded, so the program prints the states and errors of the jobs. In an application, a worker owned by the host calls it, never a tool that a model can call.
  • SearchAsync returns up to six passages within an evidence budget of 2,400 tokens by default. KnowledgeSearchRequest sets other values with its Limit and EvidenceTokenBudget parameters. Notice states the limits of the result, for example that the evidence isn't exhaustive.

When the same file is uploaded again with unchanged content and unchanged processing settings, the duplicate work is skipped. A failed replacement leaves the previous version searchable.

Search Modes

KnowledgeSearchRequest accepts a KnowledgeSearchMode:

ModeSearch
AutoThe default: keyword search without embeddings, hybrid search with them
KeywordFull-text search with SQLite FTS5 and BM25 ranking over the literal query terms
SemanticExact vector search; requires embeddings
HybridCombines the top 100 results of the keyword and the vector ranking with reciprocal rank fusion

Keyword search needs no model and finds literal terms such as contract numbers. Semantic search also finds passages that express the question in other words. It needs an embedding model: KnowledgeEmbeddingProvider takes an IEmbeddingGenerator<string, Embedding<float>>, and LlamaServerEmbeddingGenerator of TXTextControl.AI.LlamaServer runs a GGUF embedding model in its own process, separate from the chat model.

  • Use a retrieval model. Chat models can't replace embedding models. The package doesn't download or choose a model; the dimensions and the pooling come from the model you choose. The blog article about the preview uses Qwen3-Embedding-0.6B-Q8_0.gguf with 1,024 dimensions and last pooling.
  • Semantic and hybrid searches don't fall back. They require compatible vectors for all sources they search.
  • Reindex after configuration changes. Changed extraction, chunking or embedding settings require reindexing the sources, for example with ReindexSourceAsync.
  • Scores rank passages. They aren't probabilities that a passage is correct.
  • Measure your collection sizes. The exact vector search scans all vectors; it isn't a distributed vector database.

Extract DOCX, PDF and HTML Documents

In a web application, the ASP.NET Core integration extracts rich documents with TX Text Control. It reads TXT and Markdown locally and sends DOCX, RTF, PDF, TX and HTML files once as a binary request to the /mcp/knowledge/extract endpoint of the MCP document server. The extractor uses the endpoint configured as LocalAI:McpEndpoint; a remote endpoint must use HTTPS and be approved in LocalAI:TrustedMcpEndpoints. The request isn't a tool call of a model, and the host of the integration doesn't need the document engine.

DocumentWhat the extraction returns
TX, RTF, DOCX, HTMLBody paragraphs and table rows in document order, with the heading and the first table row as context. Headers and footers, footnotes and text frames aren't included
PDF with textThe text lines, loaded with PDFImportSettings.GenerateLines. Headings, tables and page positions aren't reconstructed
Scanned PDFNo text; the document needs optical character recognition (OCR) before it's indexed, which isn't included

The endpoint accepts up to 32 MiB per document and returns up to two million characters in up to 100,000 blocks. Protect it with the same authorization, TLS and limits as the MCP endpoint. For other formats or an OCR step, implement IKnowledgeDocumentExtractor. It returns KnowledgeBlock objects with the text and a stable locator, and its Version changes whenever the extraction behaves differently.

Ground Answers and Cite Sources

TXTextControl.AI.AspNetCore combines retrieval and generation. On the host that runs the AI integration, register the knowledge store next to the existing AI registration:

using TXTextControl.AI.AspNetCore;
using TXTextControl.AI.Knowledge;

var builder = WebApplication.CreateBuilder(args);

// Authentication and the policies "AiUsers" and "AiAdministrators" are configured by your application
builder.Services.AddTextControlAI(builder.Configuration.GetSection("LocalAI"));

var privateDirectory = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "Knowledge");

builder.Services.AddTextControlAIKnowledge(new KnowledgeWebOptions(
    new KnowledgeOptions
    {
        DatabasePath = Path.Combine(privateDirectory, "knowledge.db")
    }, "AiUsers")
{
    AdministrationPolicy = "AiAdministrators",
    EvidenceTokenBudget = 2400
});

// Optional: semantic and hybrid search with a local embedding model
builder.Services.AddTextControlAIKnowledgeEmbeddings(
    Path.GetFullPath("Models", builder.Environment.ContentRootPath),
    Path.Combine(privateDirectory, "embedding.json"));

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseTextControlAI();
app.MapTextControlAI(administrationPolicy: "AiAdministrators").RequireAuthorization("AiUsers");
app.MapTextControlAIKnowledge();
app.Run();

The code works as follows:

  • AddTextControlAIKnowledge registers the store, the indexing worker and the extractor for rich documents. "AiUsers" is the authorization policy that guards all knowledge endpoints, and AdministrationPolicy guards the embedding settings; without it, the embedding administration only requires the access policy.
  • AddTextControlAIKnowledgeEmbeddings is optional. Keyword search works without it. Changed embedding settings require a restart of the host and reindexing.
  • MapTextControlAIKnowledge maps the endpoints for collections, uploads, jobs, sources and search under /api/knowledge, after the authentication and authorization middleware. AddTextControlAI and MapTextControlAI stay required for generation, as Add the AI Endpoints to ASP.NET Core describes.

A page passes the collection with the chat request, and the answer comes back with its citations:

const ai = new TXTextControlAI.Client({ apiBaseUrl: "/api" });
const collections = await ai.knowledgeCollections();
const collectionId = collections[0].id;

const answer = await ai.chat("How much may a hotel night cost?", event => {
    if (event.type === "status") console.log(event.message);
}, {
    knowledge: { collectionId, allowDocumentChanges: false }
});

console.log(answer.text);

for (const citation of answer.citations ?? []) {
    console.log(citation.citationId, citation.source.fileName, citation.source.locator);
}

The integration adds the passages to the request as numbered evidence and gives the model rules for it:

  • Evidence is data. Retrieved passages are never instructions and never permission to call tools.
  • Claims need evidence. Statements about private documents rest only on the passages and cite them by the citation IDs of the evidence. Missing or contradictory evidence is reported; prices, terms and other missing values aren't invented.
  • Citations are checked. A citation ID that isn't part of the evidence is marked as unverified. A source that was replaced or deleted after the answer returns "unavailable" instead of another version.
  • Evidence isn't truncated. When the evidence doesn't fit into the context, the request fails instead of answering with part of the evidence.

In the WebDocumentAssistant sample, an answer with reference knowledge lists its citations below the text, each with the file name and the locator of the passage:

An answer of the WebDocumentAssistant sample that cites passages as K1 to K6, with the list of cited PDF files and their text lines below

Retrieval returns the best-ranked passages, not every relevant one. A question such as "list all contracts with a 60-day payment term" or "total all invoices" can't be answered reliably from them, so exhaustive reviews need a workflow of their own. Citations make answers easier to check, but they don't prove that an answer is correct.

Apply Approved Wording

Requests with reference knowledge are read-only by default. To replace a clause with approved wording, the request sets allowDocumentChanges: true and asks explicitly for the change. The setting permits only changes the user requested; requests to review, compare or check a document stay read-only. When the task calls for one particular document, the request restricts retrieval to its version with sourceVersionId. Agent Workflows describes how document changes are applied and verified.

Prepare Documents for Retrieval

  • Start small. A small, curated collection is a better start than a large one. Review the extracted text, confirm that each source is current, test representative questions and check that users only see what they're allowed to see.
  • Use heading styles. Passages carry their heading as context, which only works when headings are formatted with heading styles. Prepare Documents for Analysis describes structure, Markdown and custom heading styles.
  • Give tables a header row. The first row gives the other rows their context.
  • Know what isn't indexed. Headers and footers, footnotes and text frames aren't part of the extraction of rich documents.
  • Replace sources instead of adding copies. Uploading a document under the same file name replaces its previous version and removes the old passages from the index.

Protect the Knowledge Store

  • Access. OwnerKnowledgeAccessPolicy denies access unless the authenticated subject owns the collection. For tenants or shared collections, implement IKnowledgeAccessPolicy. IDs returned by the store aren't permissions.
  • Storage. Keep the database outside the static files of the site, and source texts, file names and embeddings out of static hosting, logs and model instructions. SQLite encryption isn't built in, so apply file system permissions and, if required, encrypted disks.
  • Deletion. Deleting a collection or source isn't secure erasure: SQLite free pages, the WAL file and backups can still contain the data.
  • External inference. Indexing and embeddings can stay local while the chat model runs elsewhere, but the passages in the prompts then leave the host. A remote inference endpoint for private knowledge must be approved in LocalAI:TrustedInferenceEndpoints.

Next Steps