SQLite dictionary database optimization - android

I am writing a dictionary application. I have a large static SQLite database with one table (100k+ rows) and an »_id«, »word« and »description« columns. Entries are lexicographically ordered by »word«. I use the code below and afterTextChanged(Editable searchText) to make instant searches in the database, loading a selection of rows into an ArrayList, and from there to a list adapter.
The code works, however, it is not very efficient, i.e.:
Searches for words beginning with »z« are much slower than those starting with an »a«.
After inputing »abc«, when user inputs »d«, search for »abcd« starts from the begining instead from the first occurence of »abc«.
Queries for nonexisting strings last very long, but we know that say, »abcdx« can only appear between »abcd« and »abce«.
Is there a way how I can optimize for those properties? Are there any ready-made solutions for dictionaries? Sorry, I am new to this.
I canot replace »LIKE« with »>=« for example, since words also consists of some non-ASCII characters and »>=« query doesn't work with them.
//---retrieves a database subset in the range (MATCH-RowsBefore, MATCH+RowsAfter)---
public Cursor getSearch(Editable typedKeys) throws SQLException
{
Cursor sCursor =
db.rawQuery("SELECT _id, word FROM dict WHERE word LIKE ? LIMIT 1",
new String[]{typedKeys + "%"});
if (sCursor.moveToFirst()) {
SearchedRow = Integer.parseInt(sCursor.getString(0));
int newstart = SearchedRow - RowsBefore;
String curlen = Integer.toString(RowsBefore + RowsAfter + 1);
sCursor = db.query(true, DB_TABLE, new String[] {_id,
word}, _id + ">=" + newstart, null, null, null, null, curlen);
}
return sCursor;
}

Related

How should I write a SQLite query so it searches for strings in a database that match strings in a string array?

I have a database that holds recipes. I want to be able to search the database for recipes by ingredients. So, if I search for "apple", it should pull up all the recipes that have the word "apple" in the column "ingredients."
Example of table
Name: "Apple pie"
Ingredients: "apple flour sugar etc."
At first, when it searched the database, I had it set up so that the inputText was just a string. For example, the inputText would be like "apple flour." But, I thought maybe it would make more sense if it took the inputText and put it in a String Array (but I could be totally wrong). So, it would be more like this: ("apple", "flour").
But, my issue is that I don't know how I would write the query so that it looks in the ingredients column for any string that matches one in the array.
Also, should I make it so that the strings in the ingredients column are arrays as well? Because right now they're just strings separated by spaces.
Here's what my query looks like now, which I know isn't right
public Cursor fetchRecipesByName(String inputText) throws SQLException {
SQLiteDatabase mDb = this.getWritableDatabase();
Log.w(TAG, inputText);
Cursor mCursor = null;
if (inputText == null || inputText.length () == 0) {
mCursor = mDb.query(SQLITE_TABLE, new String[] {COLUMN_ROWID,
COLUMN_NAME, COLUMN_TYPE, COLUMN_INGRED, COLUMN_SPECIAL, COLUMN_DESCRIPT, COLUMN_ALLINGRED, COLUMN_INSTRUCT, COLUMN_IMGPATH},
null, null, null, null, null);
}
else {
mCursor = mDb.query(true, SQLITE_TABLE, new String[] {COLUMN_ROWID,
COLUMN_NAME, COLUMN_TYPE, COLUMN_INGRED, COLUMN_SPECIAL, COLUMN_DESCRIPT, COLUMN_ALLINGRED, COLUMN_INSTRUCT, COLUMN_IMGPATH},
COLUMN_INGRED + " like '%" + inputText + "%'",
null, null, null, null, null);
}
if (mCursor != null) {
mCursor.moveToFirst();
}
return mCursor;
}
The correct way
What you have is a multi to multi relationship. Ie, you have many recipes which can each have many ingredients... or you have many ingredients which can be in many recipes.
The best way to handle the multi-to-multi relationship is by having 3 tables. One for your recipes, one for your ingredients, and one for relations.
For your Recipes Table, let's say you have columns _id and name (and others, but those aren't relevant)
For Ingredients Table, you would also have _id and name (and again, lets ignore other columns for this example)
For the relations table, say RecipesAndIngredients, you would have columns recipe_id and ingredient_id
Then to search for ingredients named 'apple' and 'flour', your end-result
SQL statement would be:
SELECT _id,name FROM Recipes
JOIN RecipesAndIngredients
ON RecipesAndIngredients.recipe_id=Recipes._id
JOIN Ingredients
ON Ingredients.id = RecipesAndIngredients.ingredient_id
WHERE Ingredients.name LIKE 'apple%' OR Ingredients.name LIKE 'flour%'
GROUP BY Recipes._id
I'm not going to work this into an Android-friendly example, but it is the correct way to do it.
The easy way
Use someone else's answer.
Why not use the easy way?
Performance. Search will be slower because indexes cannot be used for LIKE
'%abc', and you will have to use LIKE '%abc%' for everything since
you wouldn't know WHERE in the ingredients list the 'abc' is.
Consistency. There are a finite number of ingredients, and storing strings means you could end up with 'flour', 'floor', 'fluur' or whatever else. By using a separate table, users could first search for an ingredient before adding it.
Data access. I'm not gonna get into it, but you have way more options for accessing your data, such as "how many recipes have apples in them?" or "What recipes have 3 of the same ingredients that are in THIS RECIPE?".
Learning. Using my way will help you learn databases as well as Android. The easy way will only really help with learning Android.
Why use the easy way?
It's easier. You already have the code written.
Less Learning. It will also keep you from learning about databases, which I see as a bad thing because databases are great. But, if you don't want to learn databases right now, then my approach won't be very good for you.
Overwhelming...ness. Learning how databases work is different than learning how Java/Android works, and trying to understand it can be a bit overwhelming.
Notes
There are third party database libraries/tools available. I
personally recommend using one, such as Greendao or SugarORM. Then you won't have to do any real databasing.
Threading/Synchronization sucks with Android SQL, and seemingly random errors pop up as a result. I made this project to solve that problem.
Lets say you named the array which contains ingredients you want to search, ings. I mean ings is the string array you build from splitting inputText.
You can do this:
String qr = "";
for(int i=0; i<ings.length; i++{
String tmp = COLUMN_INGRED + " LIKE ?"
ings[i] = "%" + ings[i] + "%";
qr += tmp;
if(i != ings.size()-1) qr += " OR ";
}
mCursor = mDb.query(true, SQLITE_TABLE, new String[] {COLUMN_ROWID,
COLUMN_NAME, COLUMN_TYPE,
COLUMN_INGRED, COLUMN_SPECIAL,
COLUMN_DESCRIPT, COLUMN_ALLINGRED,
COLUMN_INSTRUCT, COLUMN_IMGPATH},
qr,
ings, null, null, null, null);
I again mention that the type of ings is String[].
You missed to write the question mark after the "like" part and the percent % after the searched word, so your mCursor must look something like this:
Cursor mCursor = mDb.query(SQLITE_TABLE, new String[] {COLUMN_ROWID, COLUMN_NAME,
COLUMN_TYPE, COLUMN_INGRED, COLUMN_SPECIAL, COLUMN_DESCRIPT,
COLUMN_ALLINGRED, COLUMN_INSTRUCT, COLUMN_IMGPATH},
COLUMN_INGRED + " LIKE '?'", new String[]{inputText+"%"},
null, null, null);
Although I have never used it, your usage of the LIKE clause looks fine. There may be some ways you could improve on your method, but it should work. If you are looking to search for several ingredients that appear in a String[], then perhaps you could try something like this (this is untested, just an idea):
To get your String[] composed of your user's search input, split into separate search terms:
String[] splitStringBySpace(String userEditTextInput){
if (userEditTextInput.trim().equals("") {
return new String[0];
}
return userEditTextInput.trim().split("\\s+");
}
Then use it inside your revised method:
public Cursor fetchRecipesByName(String whereArgsNotSplit) throws SQLException {
String[] whereArgs = splitStringBySpace(whereArgsNotSplit);
Cursor returnCursor = null;
if (whereArgs == null || whereArgs.length == 0) {
return returnCursor;
}
/*
*Exit early if whereArgs is empty or null. If you want to return a full cursor here,
*then you can return a query() with all parameters set to null except for the
*SQLITE_TABLE.
*/
SQLiteDatabase mDb = this.getReadableDatabase(); //We are only reading the database
StringBuilder selection = new StringBuilder();
for (int i = 0; i < whereArgs.length; i++) {
whereArgs[i] = "'%" + whereArgs[i].trim() + "%'"
if (i == 0) {
selection.append(COLUMN_INGRED + " LIKE ?");
} else {
selection.append(" OR LIKE ?")
}
}
//This for loop will construct the data for our WHERE clause in the query method.
returnCursor = mDb.query(true, SQLITE_TABLE, null,
selection.toString(),
whereArgs, null, null, null, null); //Using whereArgs parameter
}
return returnCursor;
}

Android - populate ListView SQLite, cursor null pointer

I have two tables atm, users and notes. I am trying to retrieve data that belongs to the user. So all data to list must be owned by the original user and shown only to him. I have made my table in Databasehelper.
I have made a new class that controls the notes table. In listNotes() I want to loop through the cursor row and get all data owned by the user. Am I quering it correctly?
// Listing all notes
public Cursor listNotes() {
Cursor c = db.query(help.NOTE_TABLE, new String[]{help.COLUMN_TITLE,help.COLUMN_BODY, help.COLUMN_DATE}, null, null, null, null, null);
if (c != null) {
c.moveToFirst();
}
db.close();
return c;
}
I then want to display the cursor data collected in a listview
public void populateList(){
Cursor cursor = control.listNotes();
getActivity().startManagingCursor(cursor);
//Mapping the fields cursor to text views
String[] fields = new String[]{help.COLUMN_TITLE,help.COLUMN_BODY, help.COLUMN_DATE};
int [] text = new int[] {R.id.item_title,R.id.item_body, R.id.item_date};
adapter = new SimpleCursorAdapter(getActivity(),R.layout.list_layout,cursor, fields, text,0);
//Calling list object instance
listView = (ListView) getView().findViewById(android.R.id.list);
adapter.notifyDataSetChanged();
listView.setAdapter(adapter);
}
You aren't creating the NOTE_TABLE right.
You miss a space and a comma here
+ COLUMN_DATE + "DATETIME DEFAULT CURRENT_TIMESTAMP"
It has to be
+ COLUMN_DATE + " DATETIME DEFAULT CURRENT_TIMESTAMP,"
There are two issues here:
One is you have missed a comma (after the Timestamp as specified in an earlier answer).
The other error you have is when using a SimpleCursorAdapter, you need to ensure that the Projection string array includes something to index the rows uniquely and this must be an integer column named as "_id". SQLite already has a feature built in for this and provides a column named "_id" for this purpose (however you can have your own integer column which you can rename to _id). To solve this, change your projection string array to something like:
new String[] {"ROW_ID AS _id", help.COLUMN_TITLE,help.COLUMN_BODY, help.COLUMN_DATE}
I guess the NullPointerException stems from this (but without the stacktrace I don't know for sure).

Fastest way to search through strings stored in sqlite database

I have large number of strings, approximately 15,000 that I stored in a SQLite database using the following code:
void addKey(String key, String value, String table) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(KEY_KEY, key); // Contact Name
values.put(KEY_VALUE, value); // Contact Phone
// Inserting Row
db.insert(table, null, values);
db.close(); // Closing database connection
}
And then i search through that database using the following method in order to pick out any strings that match the key im looking for:
public String searchKeyString(String key, String table){
String rtn = "";
Log.d("searchKeyString",table);
// Select All Query
String selectQuery = "SELECT * FROM " + table;
SQLiteDatabase db = this.getWritableDatabase();
Cursor cursor = db.rawQuery(selectQuery, null);
// looping through all rows and adding to list
if (cursor.moveToFirst()) {
do {
Log.d("searchKeyString","searching");
if(cursor.getString(1).equals(key))
rtn = rtn + "," + cursor.getString(2);
} while (cursor.moveToNext());
}
cursor.close();
db.close();
Log.d("searchKeyString","finish search");
return rtn;
}
The goal is to do this in real time as the user is typing on the keep board so response time is key and the way it stands now it takes over a second to run through the search.
I considered reading all of the items into an array list initially and sorting through that which might be faster, but i thought an array list of that size might cause memory issues. What is the best way to search through these entries in my database?
A couple of things you can do...
Change the return to a StringBuilder until the end.
Only use a readable version of the database (that's probably not making much difference though)
Do not get a new instance of the database every time, keep it opened until you don't need it anymore
Query for only what you need with the "WHERE" argument in the SQL query.
See the code below with some changes:
// move this somewhere else in your Activity or such
SQLiteDatabase db = this.getReadableDatabase();
public String searchKeyString(String key, String table){
StringBuilder rtn = new StringBuilder();
Log.d("searchKeyString",table);
// Select All Query
String selectQuery = "SELECT * FROM " + table + " WHERE KEY_KEY=?";
Cursor cursor = db.rawQuery(selectQuery, new String[] {key});
// you can change it to
// db.rawQuery("SELECT * FROM "+table+" WHERE KEY_KEY LIKE ?", new String[] {key+"%"});
// if you want to get everything starting with that key value
// looping through all rows and adding to list
if (cursor.moveToFirst()) {
do {
Log.d("searchKeyString","searching");
rtn.append(",").append(cursor.getString(2));
} while (cursor.moveToNext());
}
cursor.close();
Log.d("searchKeyString","finish search");
return rtn.toString();
}
Note even if you want this to happen in "real-time" for the user, you will still need to move this to a separate Thread or ASyncTask or you are going to run into problems....
You should consider using SELECT * FROM your-table LIMIT 50, for example. And you can put two buttons "Back", "Next" on your view. If every page has max 50 items, the user is at page 1, and he taps "Next", then you can use this query:
SELECT * FROM your-table LIMIT 50 OFFSET 50
If your table contains most of text-data, and you want to integrate search deeply into your app, consider using virtual table with FTS.
Let sqlite do the hard lifting.
First off, add an index to the field you're searching for, if you don't have one already. Secondly, don't do a SELECT all with manual table scan, but rather use a query in the form
SELECT column_value
FROM my_table
WHERE column_key LIKE "ABC%"
This returns the least amount of data, and the sql engine uses the index.
i dunno about better but maybe it'd be faster to make queries for the selected strings one by one.
public String searchKeyString(String key, String table){
String rtn = "";
Log.d("searchKeyString",table);
// Select All Query
String selectQuery = "SELECT * FROM " + table + "WHERE column_1 = " + key;
SQLiteDatabase db = this.getWritableDatabase();
Cursor cursor = db.rawQuery(selectQuery, null);
// looping through all rows and adding to list
if (cursor.moveToFirst()) {
rtn = rtn + "," + cursor.getString(2);
}
cursor.close();
db.close();
Log.d("searchKeyString","finish search");
return rtn;
}
EDIT:
Well i dunno how those custom keyboard apps do it, but those AutoCompleteTextViews are hooked up to adapters. you could just as easily make a cursorAdapter and hook your auto-complete view to it.
http://www.outofwhatbox.com/blog/2010/11/android-autocompletetextview-sqlite-and-dependent-fields/
http://www.opgenorth.net/blog/2011/09/06/using-autocompletetextview-and-simplecursoradapter-2/

Android SQLite Repeated Elements

I have an issue with SQLite on android. Right now, I'm pulling a JSON object from a server, parsing it, and putting each sub-object in a Table with things such as the Name, Row_ID, unique ID, etc. using this code:
public void fillTable(Object[] detailedList){
for(int i=0;i<detailedList.length;++i){
Log.w("MyApp", "Creating Entry: " + Integer.toString(i));
String[] article = (String[]) detailedList[i];
createEntry(article[0], article[1], article[2], article[3], article[4], article[5]);
}
}
createEntry does what it sounds like. It takes 6 strings, and uses cv.put to make an entry. No problems.
When I try to order them however, via:
public String[] getAllTitles(int m){
Log.w("MyApp", "getTitle1");
String[] columns = new String[]{KEY_ROWID, KEY_URLID, KEY_URL, KEY_TITLE, KEY_TIME, KEY_TAGS, KEY_STATE};
Log.w("MyApp", "getTitle2");
Cursor c = ourDatabase.query(DATABASE_TABLENAME, columns, null, null, null, null, KEY_TIME);
Log.w("MyApp", "getTitle3");
String title[] = new String[m];
Log.w("MyApp", "getTitle4");
int i = 0;
int rowTitle = c.getColumnIndex(KEY_TITLE);
Log.w("MyApp", "getTitle5");
for(c.moveToFirst();i<m;c.moveToNext()){
title[i++] = c.getString(rowTitle);
Log.w("MyApp", "getTitle " + Integer.toString(i));
}
return title;
}
Each entry actually has many duplicates. I'm assuming as many duplicates as times I have synced. Is there any way to manually call the onUpgrade method, which drops the table and creates a new one, or a better way to clear out duplicates?
Secondary question, is there any way to order by reverse? I'm ordering by time now, and the oldest added entries are first (smallest number). Is there a reverse to that?
If you don't want duplicates in one column then create that column with the UNIQUE keyword. Your database will then check that you don't insert duplicates and you can even specify what should happen in that case. I guess this would be good for you:
CREATE TABLE mytable (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
theone TEXT UNIQUE ON CONFLICT REPLACE
)
If you insert something into that table that already exists it will delete the row that already has that item and inserts your new row then. That also means that the replaced row gets a new _id (because _id is set to automatically grow - you must not insert that id yourself or it will not work)
Your second question: you can specify the direction of the order of if you append ASC (ascending) or DESC (descending). You want DESC probably.
Cursor c = ourDatabase.query(DATABASE_TABLENAME, columns, null, null, null, null, KEY_TIME + " DESC");

Using the LIMIT statement in a SQLite query

I have a query that selects rows in a ListView without having a limit. But now that I have implemented a SharedPreferences that the user can select how much rows will be displayed in the ListView, my SQLite query doesn't work. I'm passing the argument this way:
return wDb.query(TABELANOME, new String[] {IDTIT, TAREFATIT, SUMARIOTIT}, CONCLUIDOTIT + "=1", null, null, null, null, "LIMIT='" + limite + "'");
The equals (=) operator is not used with the LIMIT clause. Remove it.
Here's an example LIMIT query:
SELECT column FROM table ORDER BY somethingelse LIMIT 5, 10
Or:
SELECT column FROM table ORDER BY somethingelse LIMIT 10
In your case, the correct statement would be:
return wDb.query(TABELANOME, new String[] {IDTIT, TAREFATIT, SUMARIOTIT}, CONCLUIDOTIT + "=1", null, null, null, null, String.valueOf(limite));
Take a look here at the SQLite select syntax: http://www.sqlite.org/syntaxdiagrams.html#select-stmt
This image is rather useful: http://www.sqlite.org/images/syntax/select-stmt.gif
For anyone stumbling across this answer looking for a way to use a LIMIT clause with an OFFSET, I found out from this bug that Android uses the following regex to parse the limit clause of a query:
From <framework/base/core/java/android/database/sqlite/SQLiteQueryBuilder.java>
LIMIT clause is checked with following sLimitPattern.
private static final Pattern sLimitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
Note that the regex does accept the format offsetNumber,limitNumber even though it doesn't accept the OFFSET statement directly.
Due to this bug which also doesn't allow for negative limits
8,-1
I had to use this workaround
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(table);
String query = builder.buildQuery(projection, selection, null, null, null, sortOrder, null);
query+=" LIMIT 8,-1";

Categories

Resources