Full-Text Search Tutorial
On this page
In this tutorial you will learn how to create an application which will use the following technologies:
Note that SQLite full-text search is supported in Entity Framework v4 - v6. It is not supported in Entity Framework Core.
Follow the link at the end of this article to download the complete application sample.
Prerequisites
- Create a console C# project in Visual Studio 2015 and call it SQLiteFullTextSearch.
- In the project, add references to the following assemblies:
- System.Data.Entity;
- Devart.Data;
- Devart.Data.SQLite;
- Devart.Data.SQLite.Entity;
- Devart.Data.SQLite.Entity.Migrations.
Add EF Code-First NuGet package to the project (note that Entity Framework Core does not support full-text search, so you need to use Entity Framework v6): run Package Manager Console via the Visual Studio Tools menu -> Library Package Manager -> Package Manager Console, and in the displayed window run the following command:
|
|---|
Install-Package EntityFramework |
As a result, the EntityFramework.dll assembly is added to the project, App.config is supplemented with records and the packages.config file is created.
Add a new MyContext.cs file to the project, and place an empty model in it:
|
|---|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using Devart.Data.SQLite;
namespace SQLiteFullTextSearch {
public class MyContext: DbContext {
public MyContext() {
}
public MyContext(SQLiteConnection connection)
: base(connection, false) {
}
static MyContext() {
}
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
}
}
}
|
|
|---|
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Data.Entity
Imports Devart.Data.SQLite
Namespace SQLiteFullTextSearch
Public Class MyContext
Inherits DbContext
Public Sub New()
End Sub
Public Sub New(connection As SQLiteConnection)
MyBase.New(connection, False)
End Sub
Shared Sub New()
End Sub
Protected Overrides Sub OnModelCreating(modelBuilder As DbModelBuilder)
End Sub
End Class
End Namespace
|
Run the following command in Package Manager Console:
As a result, the Configuration class is added to the project.
Complement the existing Configuration class constructor
|
|---|
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
|
|
|---|
Public Sub New()
AutomaticMigrationsEnabled = False
End Sub
|
by specifying the SQL generator:
|
|---|
public Configuration()
{
AutomaticMigrationsEnabled = false;
SetSqlGenerator(
Devart.Data.SQLite.Entity.Migrations.SQLiteConnectionInfo.InvariantName,
new Devart.Data.SQLite.Entity.Migrations.SQLiteEntityMigrationSqlGenerator());
}
|
|
|---|
Public Sub New()
AutomaticMigrationsEnabled = False
SetSqlGenerator( _
Devart.Data.SQLite.Entity.Migrations.SQLiteConnectionInfo.InvariantName, _
New Devart.Data.SQLite.Entity.Migrations.SQLiteEntityMigrationSqlGenerator())
End Sub
|
- Setting the connection string for our class of the MyContext model will be performed in the config file of the application. Add the connection string to the App.config file:
|
|---|
<connectionStrings>
<add name="MyContext" connectionString="Data Source=d:\Test.db;FailIfMissing=false"
providerName="Devart.Data.SQLite" />
</connectionStrings>
|
Creating Classes and Migration
Add the Book.cs file to the project:
|
|---|
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
namespace SQLiteFullTextSearch {
public class Book {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("rowid")]
public int Rowid { get; set; }
public string Author { get; set; }
public string Name { get; set; }
public string Content { get; set; }
[Column("Books")]
public string Fts { get; set; }
}
}
|
|
|---|
Imports System.Collections.Generic
Imports System.ComponentModel.DataAnnotations
Imports System.Linq
Imports System.Text
Namespace SQLiteFullTextSearch
Public Class Book
<Key> _
<DatabaseGenerated(DatabaseGeneratedOption.Identity)> _
<Column("rowid")> _
Public Property Rowid() As Integer
Get
Return m_Rowid
End Get
Set
m_Rowid = Value
End Set
End Property
Private m_Rowid As Integer
Public Property Author() As String
Get
Return m_Author
End Get
Set
m_Author = Value
End Set
End Property
Private m_Author As String
Public Property Name() As String
Get
Return m_Name
End Get
Set
m_Name = Value
End Set
End Property
Private m_Name As String
Public Property Content() As String
Get
Return m_Content
End Get
Set
m_Content = Value
End Set
End Property
Private m_Content As String
<Column("Books")> _
Public Property Fts() As String
Get
Return m_Fts
End Get
Set
m_Fts = Value
End Set
End Property
Private m_Fts As String
End Class
End Namespace
|
Here we use the rowid column, which is auto-generated by SQLite for all the tables, to map the entity key. The Fts property is created for performing full-text search. It is mapped to the virtual column, having the same name as the table. For more information on these properties refer to Full-Text Search Support in Entity Framework.
Add the following line to the MyContext class:
|
|---|
public DbSet<Book> Books { get; set; }
|
|
|---|
Public Property Books() As DbSet(Of Book)
Get
Return m_Books
End Get
Set
m_Books = Value
End Set
End Property
Private m_Books As DbSet(Of Book)
|
To generate migration, run the following command in Package Manager Console:
As a result, a migration class with the following content is added to the project:
|
|---|
public partial class AddBook : DbMigration
{
public override void Up()
{
CreateTable(
"Books",
c => new
{
Rowid = c.Int(nullable: false, identity: true),
Author = c.String(maxLength: 200),
Name = c.String(maxLength: 300),
Content = c.String(),
Books = c.String(),
})
.PrimaryKey(t => t.Rowid);
}
public override void Down()
{
DropTable("Books");
}
}
|
|
|---|
Public Partial Class AddBook
Inherits DbMigration
Public Overrides Sub Up()
CreateTable("Books", Function(c) New With {
.Rowid = c.Int(nullable := False, identity := True),
.Author = c.[String](maxLength := 200),
.Name = c.[String](maxLength := 300),
.Content = c.[String](),
.Books = c.[String]()
}).PrimaryKey(Function(t) t.Rowid)
End Sub
Public Overrides Sub Down()
DropTable("Books")
End Sub
End Class
|
-
Complement the migration by specifying the storage engine type for the table, and creating a full-text index on the Content column, without specifying explicitly the name of the generated index:
|
|---|
using Devart.Data.SQLite.Entity;
using Devart.Data.SQLite.Entity.Migrations;
public partial class AddBook : DbMigration
{
public override void Up()
{
CreateTable(
"Books",
c => new
{
Rowid = c.Int(nullable: false, identity: true),
Author = c.String(maxLength: 200),
Name = c.String(maxLength: 300),
Content = c.String(),
Books = c.String(),
},
anonymousArguments: new SQLiteCreateTableConfiguration()
{
TableType = SQLiteTableType.Fts4,
FtsTokenizer = SQLiteFtsTokenizer.Porter
})
.PrimaryKey(t => t.Rowid);
}
public override void Down()
{
DropTable("Books");
}
}
|
|
|---|
Imports Devart.Data.SQLite.Entity
Imports Devart.Data.SQLite.Entity.Migrations
Public Partial Class AddBook
Inherits DbMigration
Public Overrides Sub Up()
CreateTable("Books", Function(c) New With {
.Rowid = c.Int(nullable := False, identity := True),
.Author = c.[String](maxLength := 200),
.Name = c.[String](maxLength := 300),
.Content = c.[String](),
.Books = c.[String]()
}, anonymousArguments := New SQLiteCreateTableConfiguration() With { _
.TableType = SQLiteTableType.Fts4, _
.FtsTokenizer = SQLiteFtsTokenizer.Porter _
}).PrimaryKey(Function(t) t.Rowid)
End Sub
Public Overrides Sub Down()
DropTable("Books")
End Sub
End Class
|
Update the database using the migration, by running the following command in Package Manager Console:
As a result, commands for model objects creation will be executed
|
|---|
CREATE VIRTUAL TABLE Books USING fts4(Author, Name, Content, tokenize=porter)
|
Also, commands for creating the system MigrationHistory table and filling it with migration data will be executed.
Full-Text Search Sample
In this tutorial, we have turned on SQLiteMonitor to view executed DDL and DML statements in the Devart dbMonitor application. However, keep in mind that the use of SQLiteMonitor is feasible for the purpose of testing and debugging but should be limited in production environments, since monitoring can decrease the performance of your application.
To use monitoring, add this code to your application
|
|---|
var monitor = new SQLiteMonitor() { IsActive = true };
|
|
|---|
Dim monitor = New SQLiteMonitor() With { _
.IsActive = True _
}
|
and run dbMonitor.
Let's write the code filling the table with data and forming full-text search queries.
|
|---|
using (var ctx = new SQLiteFullTextSearch.MyContext()) {
// Load information into the Books table
ctx.Books.Add(new SQLiteFullTextSearch.Book() {
Author = "William Shakespeare",
Name = "The Tragedy of King Richard the Second",
Content = @"ACT 1
SCENE I. London. A Room in the palace.
[Enter KING RICHARD, attended; JOHN OF GAUNT, with other NOBLES.]
KING RICHARD.
Old John of Gaunt, time-honoured Lancaster,
Hast thou, according to thy oath and band,
Brought hither Henry Hereford thy bold son,
Here to make good the boisterous late appeal,
Which then our leisure would not let us hear,
Against the Duke of Norfolk, Thomas Mowbray?
..."
});
ctx.Books.Add(new SQLiteFullTextSearch.Book() {
Author = "William Shakespeare",
Name = "The Life and Death of King John",
Content = @"ACT 1.
SCENE I. Northampton. A Room of State in the Palace.
[Enter KING JOHN, QUEEN ELINOR, PEMBROKE, ESSEX, SALISBURY, and others, with CHATILLON.]
KING JOHN.
Now, say, Chatillon, what would France with us?
CHATILLON.
Thus, after greeting, speaks the King of France,
In my behaviour, to the majesty,
The borrow'd majesty of England here.
..."
});
ctx.Books.Add(new SQLiteFullTextSearch.Book() {
Author = "William Shakespeare",
Name = "The Life of Henry the Fifth",
Content = @"ACT 1.
SCENE I. London. An ante-chamber in the King's palace.
[Enter the Archbishop of Canterbury and the Bishop of Ely.]
CANTERBURY.
My lord, I'll tell you: that self bill is urg'd,
Which in the eleventh year of the last king's reign
Was like, and had indeed against us pass'd,
But that the scambling and unquiet time
Did push it out of farther question.
..."
});
ctx.SaveChanges();
// Write an analogue of the SQL query:
// SELECT rowid, name FROM Books
// WHERE Books MATCH 'king palace Norfolk';
var firstQuery = ctx.Books
.Where(b => SQLiteTextFunctions.Match(b.Fts, "king palace Norfolk"))
.Select(b => new { b.Rowid, b.Name } );
var firstQueryResults = firstQuery.ToList();
// Output:
Console.WriteLine();
Console.WriteLine("== Querying the table ==");
Console.WriteLine();
Console.WriteLine("1) Natural language");
Console.WriteLine();
Console.WriteLine("SQL:");
Console.WriteLine(firstQuery.ToString());
Console.WriteLine();
Console.WriteLine("Data:");
Console.WriteLine("{0,-5} | {1}", "Rowid", "Name");
Console.WriteLine(new string('-', 50));
foreach (var item in firstQueryResults)
Console.WriteLine("{0,-5} | {1} ", item.Rowid, item.Name);
// Write an analogue of the SQL query:
// SELECT rowid, name FROM Books
// WHERE Books MATCH '"king''s palace"'
var secondQuery = ctx.Books
.Where(b => SQLiteTextFunctions.Match(
b.Content, "\"king's palace\""))
.Select(b => new { b.Rowid, b.Name } );
var secondQueryResults = secondQuery.ToList();
// Output:
Console.WriteLine();
Console.WriteLine("2) Phrase query");
Console.WriteLine();
Console.WriteLine("SQL:");
Console.WriteLine(secondQuery.ToString());
Console.WriteLine();
Console.WriteLine("Data:");
Console.WriteLine("{0,-5} | {1}", "Rowid", "Name");
Console.WriteLine(new string('-', 50));
foreach (var item in secondQueryResults)
Console.WriteLine("{0,-5} | {1} ", item.Rowid, item.Name);
Console.ReadLine();
} |
|
|---|
Using ctx = New SQLiteFullTextSearch.MyContext()
' Load information into the Books table
ctx.Books.Add(New SQLiteFullTextSearch.Book() With { _
.Author = "William Shakespeare", _
.Name = "The Tragedy of King Richard the Second", _
.Content = "ACT 1" & vbCr & vbLf & "SCENE I. London. A Room in the palace." _
& vbCr & vbLf & "[Enter KING RICHARD, attended; JOHN OF GAUNT, with other NOBLES.]" _
& vbCr & vbLf & "KING RICHARD." & vbCr & vbLf _
& " Old John of Gaunt, time-honoured Lancaster," & vbCr & vbLf _
& " Hast thou, according to thy oath and band," & vbCr & vbLf _
& " Brought hither Henry Hereford thy bold son," & vbCr & vbLf _
& " Here to make good the boisterous late appeal," & vbCr & vbLf _
& " Which then our leisure would not let us hear," & vbCr & vbLf _
& " Against the Duke of Norfolk, Thomas Mowbray?" & vbCr & vbLf & "..." _
})
ctx.Books.Add(New SQLiteFullTextSearch.Book() With { _
.Author = "William Shakespeare", _
.Name = "The Life and Death of King John", _
.Content = "ACT 1." & vbCr & vbLf & _
"SCENE I. Northampton. A Room of State in the Palace." & vbCr & vbLf & _
"[Enter KING JOHN, QUEEN ELINOR, PEMBROKE, ESSEX, SALISBURY, and others, with CHATILLON.]" _
& vbCr & vbLf & "KING JOHN." & vbCr & vbLf & _
" Now, say, Chatillon, what would France with us?" & vbCr & vbLf & _
"CHATILLON." & vbCr & vbLf & " Thus, after greeting, speaks the King of France," _
& vbCr & vbLf & " In my behaviour, to the majesty," & vbCr & vbLf & _
" The borrow'd majesty of England here." & vbCr & vbLf & "..." _
})
ctx.Books.Add(New SQLiteFullTextSearch.Book() With { _
.Author = "William Shakespeare", _
.Name = "The Life of Henry the Fifth", _
.Content = "ACT 1." & vbCr & vbLf & _
"SCENE I. London. An ante-chamber in the King's palace." & vbCr & vbLf & _
"[Enter the Archbishop of Canterbury and the Bishop of Ely.]" & vbCr & vbLf & _
"CANTERBURY." & vbCr & vbLf & " My lord, I'll tell you: that self bill is urg'd," _
& vbCr & vbLf & " Which in the eleventh year of the last king's reign" & vbCr _
& vbLf & " Was like, and had indeed against us pass'd," & vbCr & vbLf & _
" But that the scambling and unquiet time" & vbCr & vbLf & _
" Did push it out of farther question." & vbCr & vbLf & "..." _
})
ctx.SaveChanges()
' Write an analogue of the SQL query:
' SELECT rowid, name FROM Books
' WHERE Books MATCH 'king palace Norfolk';
Dim firstQuery = From b In ctx.Books _
Where SQLiteTextFunctions.Match(b.Fts, "king palace Norfolk") _
Select _
b.Rowid, _
b.Name
Dim firstQueryResults = firstQuery.ToList()
' Output:
Console.WriteLine()
Console.WriteLine("== Querying the table ==")
Console.WriteLine()
Console.WriteLine("1) Natural language")
Console.WriteLine()
Console.WriteLine("SQL:")
Console.WriteLine(firstQuery.ToString())
Console.WriteLine()
Console.WriteLine("Data:")
Console.WriteLine("{0,-5} | {1}", "Rowid", "Name")
Console.WriteLine(New String("-"C, 50))
For Each item In firstQueryResults
Console.WriteLine("{0,-5} | {1} ", item.Rowid, item.Name)
Next
' Write an analogue of the SQL query:
' SELECT rowid, name FROM Books
' WHERE Books MATCH '"king''s palace"'
Dim firstQuery = From b In ctx.Books _
Where SQLiteTextFunctions.Match(b.Fts, """king's palace""") _
Select _
b.Rowid, _
b.Name
Dim secondQueryResults = secondQuery.ToList()
' Output:
Console.WriteLine()
Console.WriteLine("2) Phrase query")
Console.WriteLine()
Console.WriteLine("SQL:")
Console.WriteLine(secondQuery.ToString())
Console.WriteLine()
Console.WriteLine("Data:")
Console.WriteLine("{0,-5} | {1}", "Rowid", "Name")
Console.WriteLine(New String("-"C, 50))
For Each item In secondQueryResults
Console.WriteLine("{0,-5} | {1} ", item.Rowid, item.Name)
Next
Console.ReadLine()
End Using
|
Conclusion
Thus, we have shown how to use SQLite full-text search functionality in Entity Framework applications, including the development process, based on Code-First Migrations. We are looking forward to receiving feedback from you via comments, the
Entity Framework forum and our
contact form, if it's necessary to rework or improve support of full-text indexing and searching in our provider.
You can download the archive with the complete version of the sample from this tutorial
here.
See Also
Full-Text Search Support