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.
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.
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.
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.
John Doe, gardener Roger Roe, driver Patrick Mark, writer Lucy Smith, teacher
We have four users in the users.txt file.
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.
sky blue rock war
forest cup cloud pen wrong
new balloon book wood page
These three text files are used to build our search index.
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
In this article we have worked with the Lucene.Net library.
Author
List all C# tutorials.