Book Excerpt: Full-Text Indexing and Searching (Part 2)
The following sections are excerpts from Version 6.9 of “The Busy Coder’s Guide to Android Development”, with slight modifications to fit the blog format. It continues the blog post series begun yesterday.
Creating a Full-Text Indexed Table
A full-text indexed table, using FTS3 or FTS4, uses SQLite’s
CREATE VIRTUAL TABLE
syntax. This indicates that you are opting
into some special table-storage behavior, rather than the stock stuff.
In the
Database/FTS
sample project, the onCreate()
method of our SQLiteOpenHelper
subclass (DatabaseHelper
) creates such a virtual table, using FTS4
for full-text indexing:
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE VIRTUAL TABLE questions USING fts4("
+"_id INTEGER PRIMARY KEY, title TEXT, "
+"link TEXT, profileImage TEXT, creationDate INTEGER, "
+"order=DESC);");
}
There are a few differences here from a typical CREATE TABLE
statement, beyond the introduction of the VIRTUAL
keyword:
-
The
USING fts4
indicates that the virtual table is employing the FTS4 full-text indexing engine. To use FTS3, just replacefts4
withfts3
. -
You can have key-value pairs in the column list, separated by equals signs, to provide options for configuring the virtual table. In this case, it will provide options for configuring the FTS4 indexing behavior. In this case, we are providing
order=DESC
, to indicate that the full-text index should be optimized for returning items in descending order. Note that these options only exist for FTS4, not FTS3. The full roster of available options is covered in the SQLite documentation.
This gives us a table that supports normal table operations but
also has a full-text index for its columns. However, there are some
limitations, notably that these tables ignore constraints. So, for
example, the PRIMARY KEY
constraint applied to the _id
column
is ignored.
Populating a Full-Text Indexed Table
Adding content to an FTS3 or FTS4 table uses the same INSERT
statements that you might use for a regular table. For example,
the DatabaseHelper
in the sample app has an insertQuestions()
method that deletes all existing rows in the questions
table,
then inserts a bunch of rows based on a supplied List
of Item
objects:
void insertQuestions(Context app, List<Item> items) {
SQLiteDatabase db=getDb(app);
db.beginTransaction();
db.delete("questions", null, null);
try {
for (Item item : items) {
Object[] args={ item.id, item.title, item.link,
item.owner.profileImage, item.creationDate};
db.execSQL("INSERT INTO questions (_id, title, "
+"link, profileImage, creationDate) "
+"VALUES (?, ?, ?, ?, ?)",
args);
}
db.setTransactionSuccessful();
}
finally {
db.endTransaction();
}
}
If those Item
objects look familiar, that is because this app
is a modified version of the Stack Overflow questions apps profiled
in the chapter on Internet access in the book.
The reason why we are deleting everything before inserting is just
to keep the sample simple. The database table will hold all of
the questions pulled from the Stack Exchange API. Each time we
run the app, we get the latest questions from that API. The vision
was to use INSERT OR REPLACE
or INSERT OR IGNORE
statements to
be able to merge content into the table. However, FTS3 and FTS4
tables ignore all constraints, as noted above, which prevents
the conflict resolution options (e.g., OR REPLACE
) from working.
Hence, rather than manually sifting through to find if there is an
existing row or not for a given ID value, this sample simply gets
rid of all existing rows. A production-grade app would likely apply
a more sophisticated algorithm.
Querying a Full-Text Indexed Table
While you can query a full-text indexed table using normal SELECT
statements, usually the point is to apply the MATCH
operator, as
is seen in the loadQuestions()
method from DatabaseHelper
:
Cursor loadQuestions(Context app, String match) {
SQLiteDatabase db=getDb(app);
if (TextUtils.isEmpty(match)) {
return(db.rawQuery("SELECT * FROM questions ORDER BY creationDate DESC",
null));
}
String[] args={ match };
return(db.rawQuery("SELECT * FROM questions WHERE title "
+"MATCH ? ORDER BY creationDate DESC", args));
}
The MATCH
operator supports a wide range of query structures,
including:
-
Keyword matches (e.g.,
Android
) -
Prefix matches (e.g.,
SQL*
) -
Phrase matches (e.g.,
"open source"
) -
NEAR
,AND
,OR
, andNOT
operators (e.g.,sqlite AND database
)
The result is the same sort of Cursor
that you would get from a
regular SELECT
statement against a non-full-text-indexed table.
In tomorrow’s post, we will explore a bit more about the demo app, such as the model fragment being used for holding onto the results of the query.