I have an android application with a SQLite database and I am trying to implement a search interface. First of all, I realize that making a FTS database is the way to go, but I thought that I could use a LIKE statement and get similar results if I use wildcards. That way the interface can be tested and the backend updated later.
Here is how I build the query:
public Cursor getMatching(String query) {
Cursor mCursor = db.query(false, TABLE, new String[] { KEY_ROWID,
KEY_NAME }, KEY_NAME + " LIKE ?",
new String[] { "%" + query + "%" }, null, null, null, null);
if (mCursor != null) {
mCursor.moveToFirst();
}
return mCursor;
}
KEY_NAME is the text column that I want results for, and query is the lowercase string that I want to match against. I would expect that it would return results where the query appears anywhere in the name, case insensitive. However, what I observe is different. The results of querying "" (empty string):
Coch**** ****
Squi*** St*****
Owe** ****
Smi** Ca****
G. Bur** Jo******
Gr******* Brown
Now when I query "o":
Owe** ****
G. Bur** Jo******
Gr******* Brown
Oddly enough, the first result is filtered out, although it contains an 'o'. When I query "ow":
Gr******* Brown
And finally when I query with "own":
null
Here is the method I use to handle the cursor:
public static void logMatches(Context context, String query) {
DBAdapter adapter = new DBAdapter(context);
adapter.open();
Cursor c = adapter.getMatching(query);
if (c == null)
return;
while (c.moveToNext()) {
String name = c.getString(c
.getColumnIndex(DBAdapter.KEY_NAME));
Log.d(TAG, name);
}
c.close();
adapter.close();
}
Where DBAdapter contains the SQLiteOpenHelper. The context I am passing in comes from a ContentProvider by using the getContext() method.
I want to implement a FTS table eventually, but my database is quite small now so I hoped that I could implement similar functionality using existing tables for now. Is it possible to do what I want with the LIKE clause? Am I making an obvious mistake?
I did not tried it myself but fts actually implemented in SQLite for android. See for example here and here.
Ah, yes. Instead of while (c.moveToNext()) {} put do { } while (c.moveToNext())
Related
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;
}
I've spent the whole day so far trying to get a select query to execute viarawquery or query, but I've had no luck so far.
The select statement I want to run is as the following:
SELECT * FROM h_word WHERE category='GRE' AND DONE=0 ORDER BY RANDOM() LIMIT 1
category is a TEXT type column and DONE is an INTEGER type with the default value of 0.
While the query works fine when executed directly in SQLite, in android,it doesn't return any results.
I've tried the below with no luck (the method is located in a class extended from SQLiteAssetHelper which itself is a helper class originally extended from SQLiteOpenHelper originaly taken from here: https://github.com/jgilfelt/android-sqlite-asset-helper:
public Cursor getRandomWord() {
Cursor c;
SQLiteDatabase db = getWritableDatabase();
c=db.rawQuery(query, null);
String query = "SELECT * FROM h_word WHERE category='GRE' AND DONE='0'
ORDER BY RANDOM() LIMIT 1 ";
c=db.rawQuery(query, new String[] {});
c.moveToFirst();
db.close();
return c;
}
I also tested with GRE instead of 'GRE' and 0 instead of '0' but it made no difference.
did the following as well:
public Cursor getRandomWord() {
Cursor c;
SQLiteDatabase db = getReadableDatabase();
c=db.query(true, "h_word", new String[] {
"_id",
"word",
"english_meaning"
},
"category" + "=?" + " AND " +
"DONE" + "=?",
new String[]{"GRE" ,"0"},
null, null, "RANDOM() LIMIT 1" , null);
c.moveToFirst();
db.close();
return c;
}
but the cursor remains empty.
Any ideas what I might be doing wrong here?
Any help would be much appreciated.
PS: when running a simple select statement without a where clause it, works fine.
After another few hours of struggling, I figured it's a bug in android's SQLiteDatabase class.
I managed to solve the problem by changing the name of the "category" column to something else.
Seems like "category" is a key word in the android SQLiteDatabase code, and makes a query return nothing when written in where clauses on the android side.
Someone else also had this problem here:
Android rawquery with dynamic Where clause
I have some trouble with a SQLite database with 1 table and 2 columns, column_id and word. I extended SQLiteAssetHelper as MyDatabase and made a constructor:
public MyDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
I need to check whether some string is in the database (in column word). I tried to modify the code from answer provided by Benjamin and dipali, but I used SQLiteAssetHelper and I can't get it to work. The method that I have in mind receives the string to search for as a parameter and returns a boolean if string is in the database.
public boolean someMethod(String s)
In addition, I tried to put the check on a background thread with AsyncTask because I have 60 strings to check.
TABLE_NAME and COLUMN_WORD should be self-explanatory.
public boolean someMethod(String s) {
SQLiteDatabase db = getReadableDatabase();
String[] columns = new String[] {COLUMN_WORD};
String where = COLUMN_WORD + " = ?";
String[] whereArgs = new String[] {s};
// select column_word from table where column_word = 's' limit 1;
Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, "1");
if (cursor.moveToFirst()) {
return true; // a row was found
}
return false; // no row was found
}
You can do this in the background, but I don't think for a query like this it's even necessary.
EDIT
There are some improvements that should be made to the above for the sake of correctness. For one thing, the Cursor should be closed since it is no longer being used. A try-finally block will ensure this:
Cursor cursor = db.query(...);
try {
return cursor.moveToFirst();
} finally {
cursor.close();
}
However, this method doesn't need to obtain a whole `Cursor. You can write it as follows and it should be more performant:
public boolean someMethod(String s) {
SQLiteDatabase db = getReadableDatabase();
String sql = "select count(*) from " + TABLE_NAME + " where "
+ COLUMN_WORD + " = " + DatabaseUtils.sqlEscapeString(s);
SQLiteStatement statement = db.compileStatement(sql);
try {
return statement.simpleQueryForLong() > 0;
} finally {
statement.close();
}
}
You could add a catch block and return false if you think it's possible (and valid) to encounter certain exceptions like SQLiteDoneException. Also note the use of DatabaseUtils.sqlEscapeString() because s is now concatenated directly into the query string and thus we should be wary of SQL injection. (If you can guarantee that s is not malicious by the time it gets passed in as the method argument, then you could theoretically skip this, but I wouldn't.)
because of possible data leaks best solution via cursor:
Cursor cursor = null;
try {
cursor = .... some query (raw or not your choice)
return cursor.moveToNext();
} finally {
if (cursor != null) {
cursor.close();
}
}
1) From API KITKAT u can use resources try()
try (cursor = ...some query)
2) if u query against VARCHAR TYPE use '...' eg. COLUMN_NAME='string_to_search'
3) dont use moveToFirst() is used when you need to start iterating from beggining
4) avoid getCount() is expensive - it iterates over many records to count them. It doesn't return a stored variable. There may be some caching on a second call, but the first call doesn't know the answer until it is counted.
I saved Data in my SQL databank.
Now I want to compare this saved data, with a string
Something like this:
String example = "house";
Now I want to check, if "house" is already in the databank, with a if clause
something like this
if ( example == [SQL Data] ) {
}
else {
}
Now, how can I accomplish this ?
Do something like
String sql = "SELECT * FROM your_table WHERE your_column = '" + example + "'";
Cursor data = database.rawQuery(sql, null);
if (cursor.moveToFirst()) {
// record exists
} else {
// record not found
}
stolen from here
Writing my reply to Sharath's comment as an answer, as the code will be messed up in a comment:
Not saying your reply is wrong, but it's really inefficient to select everything from the table and iterate over it outside the database and it shouldn't be suggested as an answer to the question, because it's a bad habbit to do like that in general.
The way I usually do it, if I want to see if some record is present in the database, I do like this. Not gonna argue about using do-while over a normal while-loop, because that's about different preferences ;)
String query = "SELECT * FROM table_name WHERE column_name=" + the_example_string_to_find;
Cursor cursor = db.rawQuery(query, null);
if(cursor.getCount() > 0) {
cursor.moveToFirst();
while(!cursor.isAfterLast()) {
// Do whatever you like with the result.
cursor.moveToNext();
}
}
// Getting Specific Record by name.
// in DB handler class make this function call it by sending search criteria.
Records getRecord(String name) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{KEY_ID, KEY_NAME, KEY_Auth_Name, KEY_B_PRICE}, KEY_ID + "=?",
new String[]{name}, null, null, null,null);
if (cursor.getCount() > 0)
cursor.moveToFirst();
Records Records = new Records(Integer.parseInt(cursor.getString(0)),
cursor.getString(1), cursor.getString(2),cursor.getString(3));
// return book
return Records;
}
you need to first fetch all the data from the database and next check the data with what you obtained from the database.
Have a look at the link sample database example
suppose you got a cursor object from the database
cursor = db.rawQuery("SELECT yourColumnName FROM "+TABLE_NAME, null);
if(!cursor.moveToFirst()){
}
else{
do {
if(cursor.getString(0).equals(example))
//do something which you want and break
break;
} while (cursor.moveToNext());
}
I am using sqlite FTS3 database, with my Android application.
public Cursor getWordMatches(String query, String[] columns) {
String selection = KEY_WORD + " MATCH ?";
String[] selectionArgs = new String[] {query+"*"};
return query(selection, selectionArgs, columns);
}
.....
private Cursor query(String selection, String[] selectionArgs, String[] columns) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(FTS_VIRTUAL_TABLE);
builder.setProjectionMap(mColumnMap);
Cursor cursor = builder.query(mDatabaseOpenHelper.getReadableDatabase(),
columns, selection, selectionArgs, null, null, null);
if (cursor == null) {
return null;
} else if (!cursor.moveToFirst()) {
cursor.close();
return null;
}
return cursor;
}
If I make a query and search for 'house' I will get:
household
house
apartment house
big house
(I am getting everything that has house in it, in regex house)
How can I get this kind of result for my query?
household
house
(So i only want results that start with house, in regex house*)
My android application FORCE CLOSES if I use LIKE statement(maybe because db has 200000 rows).
So should I somehow combine MATCH and LIKE statement?
Is it even possible to do this on Android, if it is not maybe I should try to run some regex on cursor after I get it from this query?
Full text search tables should always use MATCH and not LIKE, because a LIKE query performs a full scan of the table which defeats the purpose of creating the FTS table in the first place.
To solve your problem, trying using a tokenizer other than the default when creating your fts table:
-- Create a table using the simple tokenizer.
CREATE VIRTUAL TABLE simple USING fts3(tokenize=porter);
By default, SQLite uses a simple tokenizer which may be generating the matches you don't want. Switch to the porter tokenizer and see what happens. Worst case, you can implement your own custom tokenizer. See the SQLite documentation for further info.