ZetCode

C# Lucene.Net

last modified October 16, 2026

In this article we show how to use the Lucene.Net library. This is an introductory material covering the basics of Lucene.Net.

Lucene.Net

Lucene.Net is a .NET port of the Java Lucene search library. Lucene provides full-text indexing and searching capability.

To use Lucene, we first create an index. The index is a data structure which maps terms to the documents that contain them. The index can be stored in the file system or in memory. After the index of documents has been created, we build a query and perform a search on the index. Finally, the results that match the query are returned, ordered by their relevance score.

Lucene basic definitions

A term represents a word from text, while a phrase is a sequence of words. A query describes what we are looking for. There are multiple query types, including TermQuery, PhraseQuery, BooleanQuery, WildcardQuery, PrefixQuery, and RegexpQuery.

An analyzer builds token streams to analyze text. It represents a policy for extracting index terms from text. The StandardAnalyzer splits text into words, converts them to lowercase, and removes common English stop words such as "a", "an", or "the". An IndexSearcher searches the index.

A document is a collection of fields. Each field has a value associated with it. Fields store the terms we want to index and search.

Search results are sets of best-matching documents. The results are wrapped in a TopDocs class.

$ dotnet add package Lucene.Net --prerelease
$ dotnet add package Lucene.Net.Analysis.Common --prerelease

These are two basic packages that need to be added to our projects. Lucene.Net 4.8 has been in beta for a long time, but it is stable and widely used; therefore, we need the --prerelease option. The examples were tested with version 4.8.0-beta00018.

Lucene.Net simple example

The following is a simple Lucene.Net example.

Program.cs
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;

const LuceneVersion ver = LuceneVersion.LUCENE_48;

using var ramDir = new RAMDirectory();
using var analyzer = new StandardAnalyzer(ver);

var idxCfg = new IndexWriterConfig(ver, analyzer)
{
    OpenMode = OpenMode.CREATE
};

using var writer = new IndexWriter(ramDir, idxCfg);

string[] phrases = ["an old dog", "an old falcon in the sky", "a stormy night"];

foreach (var phrase in phrases)
{
    var doc = new Document
    {
        new TextField("phrase", phrase, Field.Store.YES)
    };

    writer.AddDocument(doc);
}

writer.Commit();

using DirectoryReader reader = writer.GetReader(applyAllDeletes: true);
var searcher = new IndexSearcher(reader);

var query = new TermQuery(new Term("phrase", "old"));
TopDocs topDocs = searcher.Search(query, n: 3);

Console.WriteLine($"Matching results: {topDocs.TotalHits}");

foreach (var sdoc in topDocs.ScoreDocs)
{
    Document mdoc = searcher.Doc(sdoc.Doc);
    Console.WriteLine(mdoc.Get("phrase"));
}

In the example, we define a simple index with three documents. We perform a basic query on the index.

using var ramDir = new RAMDirectory();

The index is stored in memory. To store it on the disk, we would use FSDirectory.Open(path).

using var analyzer = new StandardAnalyzer(ver);

We create a new StandardAnalyzer to process text. It uses the default English stop words.

var idxCfg = new IndexWriterConfig(ver, analyzer)
{
    OpenMode = OpenMode.CREATE
};

using var writer = new IndexWriter(ramDir, idxCfg);

An IndexWriter is created. It creates and maintains an index. The CREATE mode creates a new index or overwrites an existing one.

var doc = new Document
{
    new TextField("phrase", phrase, Field.Store.YES)
};

We create three documents. Each document has one field. A TextField is analyzed, that is, split into terms. With Field.Store.YES, the original value is stored in the index, so we can retrieve it from the search results.

writer.Commit();

We flush and commit the index data to the directory.

using DirectoryReader reader = writer.GetReader(applyAllDeletes: true);
var searcher = new IndexSearcher(reader);

A DirectoryReader and an IndexSearcher are created. They are used to perform the search.

var query = new TermQuery(new Term("phrase", "old"));

We define a basic TermQuery. It contains a single term. The term is not analyzed; it must match the indexed term exactly, so we write it in lowercase.

TopDocs topDocs = searcher.Search(query, n: 3);

We do a search with the given query. We look for the top three results, if any. The search returns TopDocs; its TotalHits property contains the number of all matching documents.

foreach (var sdoc in topDocs.ScoreDocs)
{
    Document mdoc = searcher.Doc(sdoc.Doc);
    Console.WriteLine(mdoc.Get("phrase"));
}

We go through the results and print the matched phrases.

$ dotnet run
Matching results: 2
an old dog
an old falcon in the sky

Lucene.Net PhraseQuery and BooleanQuery

A PhraseQuery matches documents that contain the terms in the given order next to each other. A BooleanQuery combines other queries; the Occur.MUST clauses must match, the Occur.SHOULD clauses may match, and the Occur.MUST_NOT clauses must not match.

Program.cs
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;

const LuceneVersion ver = LuceneVersion.LUCENE_48;

using var ramDir = new RAMDirectory();
using var analyzer = new StandardAnalyzer(ver);
using var writer = new IndexWriter(ramDir, new IndexWriterConfig(ver, analyzer));

string[] phrases = ["an old dog", "an old falcon in the sky",
    "a falcon is not old", "a stormy night"];

foreach (var phrase in phrases)
{
    writer.AddDocument(new Document { new TextField("phrase", phrase, Field.Store.YES) });
}

writer.Commit();

using DirectoryReader reader = writer.GetReader(applyAllDeletes: true);
var searcher = new IndexSearcher(reader);

var pquery = new PhraseQuery
{
    new Term("phrase", "old"),
    new Term("phrase", "falcon")
};

Search("phrase query", pquery);

var bquery = new BooleanQuery
{
    { new TermQuery(new Term("phrase", "old")), Occur.MUST },
    { new TermQuery(new Term("phrase", "falcon")), Occur.MUST }
};

Search("boolean query", bquery);

void Search(string title, Query query)
{
    TopDocs topDocs = searcher.Search(query, n: 10);
    Console.WriteLine($"{title}: {topDocs.TotalHits} results");

    foreach (var sdoc in topDocs.ScoreDocs)
    {
        Console.WriteLine($"  {searcher.Doc(sdoc.Doc).Get("phrase")}");
    }
}

The phrase query finds only the document with the phrase "old falcon". The boolean query finds all documents that contain both terms, in any order and at any position.

$ dotnet run
phrase query: 1 results
  an old falcon in the sky
boolean query: 2 results
  a falcon is not old
  an old falcon in the sky

Lucene.Net QueryParser

The QueryParser creates queries from the Lucene query syntax. The syntax supports fields, boolean operators, phrases, wildcards, and more.

title:"bacon" AND body:"forest"

This query searches for bacon in the title field and forest in the body field.

$ dotnet add package Lucene.Net.QueryParser --prerelease

We need to add the Lucene.Net.QueryParser package.

Program.cs
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;

const LuceneVersion ver = LuceneVersion.LUCENE_48;

using var ramDir = new RAMDirectory();
using var analyzer = new StandardAnalyzer(ver);
using var writer = new IndexWriter(ramDir, new IndexWriterConfig(ver, analyzer));

string[] phrases = ["in the sun", "before midnight", "a cloud in the sky", "stormy clouds"];

foreach (var phrase in phrases)
{
    writer.AddDocument(new Document { new TextField("phrase", phrase, Field.Store.YES) });
}

writer.Commit();

using DirectoryReader reader = writer.GetReader(applyAllDeletes: true);
var searcher = new IndexSearcher(reader);

var parser = new QueryParser(ver, "phrase", analyzer);

foreach (var text in new[] { "cloud OR midnight", "cloud*", "\"in the sky\"" })
{
    Query query = parser.Parse(text);
    TopDocs topDocs = searcher.Search(query, n: 4);

    Console.WriteLine($"{text} -> {query}");

    foreach (var sdoc in topDocs.ScoreDocs)
    {
        Console.WriteLine($"  {searcher.Doc(sdoc.Doc).Get("phrase")}");
    }
}

We create an index containing four phrases and run three queries written in the Lucene query syntax.

var parser = new QueryParser(ver, "phrase", analyzer);

The second parameter is the default field, which is searched when the query does not name a field. The parser analyzes the query text with the same analyzer as the index; therefore, the search terms are lowercased.

Console.WriteLine($"{text} -> {query}");

We print each query text and the query object created by the parser. The cloud* query is a prefix query; it also finds "clouds". The quoted text is a phrase query; the stop words "in" and "the" are removed from it.

$ dotnet run
cloud OR midnight -> phrase:cloud phrase:midnight
  before midnight
  a cloud in the sky
cloud* -> phrase:cloud*
  a cloud in the sky
  stormy clouds
"in the sky" -> phrase:sky
  a cloud in the sky

Lucene.Net WildcardQuery

The WildcardQuery implements the wildcard search query. It supports the * (matches any character sequence, including an empty one) and the ? (matches any single character) wildcards.

doc/users.txt
John Doe, gardener
Roger Roe, driver
Patrick Mark, writer
Lucy Smith, teacher

We have four users in the users.txt file.

Program.cs
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;

const LuceneVersion ver = LuceneVersion.LUCENE_48;

using var ramDir = new RAMDirectory();
using var analyzer = new StandardAnalyzer(ver);
using var writer = new IndexWriter(ramDir, new IndexWriterConfig(ver, analyzer));

List<User> users = ReadUsers("doc/users.txt");
CreateIndex(writer, users);
DoSearch(writer);

static List<User> ReadUsers(string path)
{
    var users = new List<User>();

    foreach (var line in File.ReadLines(path))
    {
        var fields = line.Split(',', StringSplitOptions.TrimEntries);
        users.Add(new User(fields[0], fields[1]));
    }

    return users;
}

static void CreateIndex(IndexWriter writer, List<User> users)
{
    foreach (var user in users)
    {
        var doc = new Document
        {
            new TextField("name", user.Name, Field.Store.YES),
            new TextField("occupation", user.Occupation, Field.Store.YES)
        };

        writer.AddDocument(doc);
    }

    writer.Commit();
}

static void DoSearch(IndexWriter writer)
{
    using DirectoryReader reader = writer.GetReader(applyAllDeletes: true);
    var searcher = new IndexSearcher(reader);

    var query = new WildcardQuery(new Term("name", "*oe"));
    TopDocs topDocs = searcher.Search(query, n: 3);

    Console.WriteLine($"Matching results: {topDocs.TotalHits}");

    foreach (var sdoc in topDocs.ScoreDocs)
    {
        Document mdoc = searcher.Doc(sdoc.Doc);
        Console.WriteLine($"{mdoc.Get("name")}, {mdoc.Get("occupation")}");
    }
}

record User(string Name, string Occupation);

The example builds an index of users from a text file. It then performs a wildcard query on the index.

var fields = line.Split(',', StringSplitOptions.TrimEntries);

We split each line at the comma. The TrimEntries option removes the spaces around the fields.

var doc = new Document
{
    new TextField("name", user.Name, Field.Store.YES),
    new TextField("occupation", user.Occupation, Field.Store.YES)
};

Each document has name and occupation fields.

var query = new WildcardQuery(new Term("name", "*oe"));

The WildcardQuery searches the name field. The indexed terms are the lowercased words of the names; we look for words that end with oe. A leading wildcard is slow on large indexes, because all terms of the field must be examined.

$ dotnet run
Matching results: 2
John Doe, gardener
Roger Roe, driver

Lucene.Net RegexpQuery

The RegexpQuery allows us to create Lucene queries utilizing regular expressions. The expression must match a whole term.

doc/words.txt
sky
blue
rock
war
doc/words2.txt
forest
cup
cloud
pen
wrong
doc/words3.txt
new
balloon
book
wood
page

These three text files are used to build our search index.

Program.cs
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;

const LuceneVersion ver = LuceneVersion.LUCENE_48;

using var ramDir = new RAMDirectory();
using var analyzer = new StandardAnalyzer(ver);
using var writer = new IndexWriter(ramDir, new IndexWriterConfig(ver, analyzer));

var files = System.IO.Directory.GetFiles("doc", "words*.txt").Order();
var words = files.SelectMany(File.ReadLines).ToList();

foreach (var word in words)
{
    writer.AddDocument(new Document { new TextField("word", word, Field.Store.YES) });
}

writer.Commit();

using DirectoryReader reader = writer.GetReader(applyAllDeletes: true);
var searcher = new IndexSearcher(reader);

var query = new RegexpQuery(new Term("word", "[wc].*"));
TopDocs topDocs = searcher.Search(query, n: 10);

Console.WriteLine($"Matching results: {topDocs.TotalHits}");

foreach (var sdoc in topDocs.ScoreDocs)
{
    Console.WriteLine(searcher.Doc(sdoc.Doc).Get("word"));
}

In the program, we build an index from the words in the text files. We apply a regex query search on the index.

var files = System.IO.Directory.GetFiles("doc", "words*.txt").Order();
var words = files.SelectMany(File.ReadLines).ToList();

We read the words from the text files. The Directory class must be fully qualified, because Lucene.Net has its own Directory class in the Lucene.Net.Store namespace.

var query = new RegexpQuery(new Term("word", "[wc].*"));

The regular expression matches all words that begin with either character w or c.

$ dotnet run
Matching results: 5
war
cup
cloud
wrong
wood

Source

Apache Lucene.Net

In this article we have worked with the Lucene.Net library.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.