---
title: "Migrate from Lucene.NET to OneIndex"
url: https://recursiveai.org/oneindex/articles/migrate-from-lucene-net/
description: "Considering a Lucene.NET alternative? Map the APIs, run both C# examples, rebuild from source, and check query behavior before switching to OneIndex."
date: 2026-11-10
author: OneIndex team
---

# Migrate from Lucene.NET to OneIndex

Considering a Lucene.NET alternative? Map the APIs, run both C# examples, rebuild from source, and check query behavior before switching to OneIndex.

November 10, 2026 · OneIndex team

If you want a **Lucene.NET alternative** that stays inside your .NET application, [OneIndex](https://recursiveai.org/oneindex/) gives you a typed document model, full-text search, and vector and hybrid search in one embedded library. To migrate from Lucene.NET, map your fields, rebuild the index from the original records, and change your search calls. **OneIndex cannot open a Lucene index and has no compatibility layer**; this is an application migration, not a package rename.

This guide runs on .NET 10 with [Lucene.NET 4.8.0-beta00017](https://www.nuget.org/packages/Lucene.Net/4.8.0-beta00017), a prerelease, and [OneIndex 0.4.0-preview.2](https://www.nuget.org/packages/RecursiveAI.OneIndex/0.4.0-preview.2), a preview. The Lucene.NET version is a fixed test target, not a claim that it is the latest release. OneIndex’s APIs and file formats may change between previews, as its [release README](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/README.md) states. Both complete programs below were run against these NuGet packages.

## When OneIndex fits the move

Consider OneIndex when you want to keep search in-process while replacing field-by-field document construction with an attributed C# class. Its typed API puts writes, commits, searches, filters, facets, and highlights behind `SearchIndex<T>`. Vector and hybrid search are available in the same library; see the [released architecture guide](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/architecture.md).

Start with an inventory if your application depends on custom Lucene analyzers, query classes, or collectors. Similar names do not promise the same behavior. OneIndex’s [migration reference](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/migrating-from-lucene-net.md) lists mappings and gaps, including no direct wildcard query, span-query family, block joins, or payloads in this preview. If any of those are central to your search, resolve that dependency before choosing a cutover date.

This article is about leaving Lucene.NET. If you only need to update an older Lucene.NET application to the 4.8 API, use Apache’s [4.8 migration guide](https://lucenenet.apache.org/docs/4.8.0-beta00016/api/core/migration-guide.html) instead.

## Map the API, not the index files

These mappings apply to the tested Lucene.NET beta and OneIndex preview. They describe corresponding jobs, not interchangeable types.

| Job | Lucene.NET 4.8.0-beta00017 | OneIndex 0.4.0-preview.2 |
|---|---|---|
| Open and search an index | `FSDirectory`, writer, reader, searcher, and parser ([example](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/overview.html)) | `SearchIndex.Open<T>(path)` ([example](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/README.md#getting-started-preview)) |
| Declare fields | `Document` with stored or unstored fields ([example](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/overview.html)) | Attributes such as `[SearchId]`, `[FullText]`, and `[Keyword]`; stored source by default ([mapping](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/mapping.md)) |
| Replace by stable ID | `UpdateDocument(new Term(...), document)` ([writer API](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/Lucene.Net.Index.IndexWriter.html)) | `Upsert(document)` using `[SearchId]` ([mapping](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/mapping.md)) |
| Delete by ID | `DeleteDocuments(new Term(...))` ([writer API](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/Lucene.Net.Index.IndexWriter.html)) | `Delete(id)`, then commit ([delete behavior](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/migrating-from-lucene-net.md#behaviour-that-differs)) |
| Parse query text | `QueryParser.Parse` with Lucene syntax ([parser API](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/queryparser/Lucene.Net.QueryParsers.Classic.QueryParser.html)) | `Search` for forgiving user text, or `SearchStrict` for field syntax and parse errors ([query mapping](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/migrating-from-lucene-net.md#queries)) |

Use your database or files as the source for the new index. Do not point OneIndex at the Lucene directory. Keep separate directories so you can build and validate the replacement without changing the running index.

## Before: the Lucene.NET program

This example indexes two products, replaces product `p1`, deletes `p2`, commits, and closes the writer. It then opens a reader and searches the committed index. The ID is a whole-value `StringField`; the title is an analyzed `TextField`. Both are stored so the result can return the original values.

Create a console project named `LuceneMigrationExample`, then install all three Lucene.NET packages at the same version:

*Shell · .NET 10*

```
dotnet new console -n LuceneMigrationExample -f net10.0
cd LuceneMigrationExample
dotnet add package Lucene.Net --version 4.8.0-beta00017
dotnet add package Lucene.Net.Analysis.Common --version 4.8.0-beta00017
dotnet add package Lucene.Net.QueryParser --version 4.8.0-beta00017
```

Replace `Program.cs` with this complete program, then run `dotnet run`. The structure follows Apache’s [indexing and searching example](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/overview.html); the replace and delete calls are documented on [IndexWriter](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/Lucene.Net.Index.IndexWriter.html).

*Program.cs · Lucene.NET · .NET 10*

```
using System;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;
 
using var directory = FSDirectory.Open("lucene-data");
using var analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
var config = new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer);
using (var writer = new IndexWriter(directory, config))
{
    void Upsert(string id, string title)
    {
        var document = new Document
        {
            new StringField("id", id, Field.Store.YES),
            new TextField("title", title, Field.Store.YES)
        };
        writer.UpdateDocument(new Term("id", id), document);
    }
 
    Upsert("p1", "Wired headphones");
    Upsert("p2", "Wireless speaker");
    Upsert("p1", "Wireless headphones");
    writer.DeleteDocuments(new Term("id", "p2"));
    writer.Commit();
}
 
using var reader = DirectoryReader.Open(directory);
var searcher = new IndexSearcher(reader);
var parser = new QueryParser(LuceneVersion.LUCENE_48, "title", analyzer);
var results = searcher.Search(parser.Parse("wireless"), 10);
foreach (var hit in results.ScoreDocs)
{
    var document = searcher.Doc(hit.Doc);
    Console.WriteLine($"{document.Get("id")}: {document.Get("title")}");
}
```

## After: the OneIndex program

The stable ID and original title stay the same. The field declarations move onto a class: `[SearchId]` identifies the replacement key, and `[FullText]` makes the title searchable. `Upsert` replaces by that key; `Delete` removes by it. Closing and reopening verifies the committed index rather than a writer’s uncommitted view.

Create a separate console project. Use an application name such as `OneIndexMigrationExample`, not `OneIndex`, to avoid an assembly-name collision with the library.

*Shell · .NET 10*

```
dotnet new console -n OneIndexMigrationExample -f net10.0
cd OneIndexMigrationExample
dotnet add package RecursiveAI.OneIndex --version 0.4.0-preview.2
```

Replace `Program.cs` with this program and run `dotnet run`:

*Program.cs · OneIndex preview · .NET 10*

```
using System;
using OneIndex;
 
using (var index = SearchIndex.Open<Product>("oneindex-data"))
{
    index.Upsert(new Product { Id = "p1", Title = "Wired headphones" });
    index.Upsert(new Product { Id = "p2", Title = "Wireless speaker" });
    index.Upsert(new Product { Id = "p1", Title = "Wireless headphones" });
    index.Delete("p2");
    index.Commit();
}
 
using var reopened = SearchIndex.Open<Product>("oneindex-data");
var results = reopened.Search("wireless").Take(10).Execute();
foreach (var hit in results)
    Console.WriteLine($"{hit.Id}: {hit.Document.Title}");
 
public class Product
{
    [SearchId] public string Id { get; set; } = "";
    [FullText] public string Title { get; set; } = "";
}
```

Both programs print exactly:

*Output · both programs · .NET 10*

```
p1: Wireless headphones
```

The test checks replacement, deletion, stored result text, and reopening after a commit. It does not establish matching token streams, scores, or rankings for a real corpus. For an introduction to the typed API, see [getting started with OneIndex](https://recursiveai.org/oneindex/docs/getting-started/).

## Check queries and ranking before switching

### Choose a query entry point deliberately

Do not pass every existing Lucene query string straight into `Search`. Apache’s [QueryParser documentation](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/queryparser/Lucene.Net.QueryParsers.Classic.QueryParser.html) defines its query language. OneIndex’s [user-text parser](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/user-text.md) is forgiving: malformed input is treated as text rather than rejected. Its [strict parser](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/query-syntax.md) supports a documented field syntax with positioned errors. Neither choice is a promise of Lucene query-string compatibility.

Classify callers first: a search box usually wants forgiving text; a saved advanced query needs a deliberate syntax translation. Field names also change if you move from lowercase Lucene names to C# property names. Review saved filters, field-qualified queries, and any API that accepts raw query strings.

### Test the analyzer, not just its name

The sample uses Lucene.NET’s `StandardAnalyzer` and OneIndex’s default `standard` analyzer. Lucene.NET’s analyzer includes a stop-word filter, documented on [StandardAnalyzer](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/analysis-common/Lucene.Net.Analysis.Standard.StandardAnalyzer.html). OneIndex’s [analysis guide](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/analysis.md) describes its standard analyzer without stop-word removal. Test stop words, punctuation, accents, identifiers, and language-specific stemming from your own data. Set a language or register a custom analyzer where your schema needs one.

### Retune score rules instead of copying numbers

Lucene.NET 4.8’s [DefaultSimilarity](https://lucenenet.apache.org/docs/4.8.0-beta00017/api/core/Lucene.Net.Search.Similarities.DefaultSimilarity.html) uses the classic TF-IDF model. OneIndex uses [BM25F](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/scoring.md). If your application already selects a different Lucene similarity, record that too. Do not carry over score thresholds or assume equal boost values mean equal ranking. Compare useful results by stable document ID, and judge them against expected answers.

Also check totals: OneIndex documents `TotalHits` as a lower bound unless you request `CountExactly()`. A page count or export job that needs an exact number should ask for it explicitly; see [the API notes](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/README.md#getting-started-preview).

## Rebuild and cut over without losing changes

1. **Record the current contract.** List indexed fields, stored fields, analyzers, sort rules, filters, query features, and update paths. Save representative queries and their expected IDs, including empty results and deleted records.
2. **Build a new index from a known source snapshot.** Map each record to the new type with the same stable ID. Batch records through `Upsert` and commit. Keep a source checkpoint so you know which later changes still need applying.
3. **Catch up writes and deletes.** Replay changes after the checkpoint, in order, including deletions. A change log or a short write pause can close the gap. A bulk copy alone is not a complete migration while the source is still changing.
4. **Compare behavior.** Check live record counts, stored values, expected result IDs, filters, sort order, paging, and malformed input. Compare ranked results against your acceptance cases, not raw scores from the two engines.
5. **Switch reads, then keep a rollback path.** Choose a final checkpoint, apply remaining changes, commit, and route searches to OneIndex. Keep the Lucene index and its update path available during your rollback window. Otherwise, switching back can serve stale data.

If only the Lucene index remains, export with Lucene.NET first. Recover original values from stored fields, not analyzed tokens. Use stable application IDs and skip deleted documents; do not treat internal Lucene document numbers as your migration keys. Fields that were never stored need another source. OneIndex’s [existing-index guidance](https://github.com/recursive-ai-oss/oneindex-csharp/blob/v0.4.0-preview.2/docs/migrating-from-lucene-net.md#existing-indexes) explains the rebuild requirement.

The goal is a new search implementation with a tested contract, not identical files or identical scores. Start with the two programs above, map your real schema with the [OneIndex migration reference](https://recursiveai.org/oneindex/docs/migrating-from-lucene-net/), and keep the first rebuild separate from the index serving your users.
