/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> === * * Copyright 2014, Enno Gottschalk <mrmaffen@googlemail.com> * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Tomahawk is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>. */ package org.tomahawk.libtomahawk.resolver; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.IntField; import org.apache.lucene.document.StringField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.SearcherFactory; import org.apache.lucene.search.SearcherManager; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; import org.tomahawk.libtomahawk.database.CollectionDb; import org.tomahawk.tomahawk_android.TomahawkApp; import org.tomahawk.tomahawk_android.utils.PreferenceUtils; import android.database.Cursor; import android.util.Log; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FuzzyIndex { private final static String TAG = FuzzyIndex.class.getSimpleName(); private static final String LUCENE_ROOT_FOLDER = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + File.separator + "lucene" + File.separator; private static final String LAST_FUZZY_INDEX_UPDATE_SUFFIX = "_last_fuzzy_index_update"; private final String mLastUpdateStorageKey; private CollectionDb mCollectionDb; private String mLucenePath; private IndexWriter mLuceneWriter; private SearcherManager mSearcherManager; public static class IndexResult { public int id; public float score; } public FuzzyIndex(CollectionDb collectionDb) { Log.d(TAG, "FuzzyIndex constructor called: " + collectionDb.getCollectionId()); mCollectionDb = collectionDb; mLucenePath = LUCENE_ROOT_FOLDER + collectionDb.getCollectionId(); mLastUpdateStorageKey = collectionDb.getCollectionId() + LAST_FUZZY_INDEX_UPDATE_SUFFIX; ensureIndex(); } /** * Make sure that the FuzzyIndex contains all tracks that are stored in the CollectionDb. * * @return whether or not the process has been successful */ public synchronized void ensureIndex() { Log.d(TAG, "addToIndex - using CollectionDb " + mCollectionDb.hashCode() + " with id " + mCollectionDb.getCollectionId()); long lastDbUpdate = mCollectionDb.getLastUpdated(); long lastIndexUpdate = PreferenceUtils.getLong(mLastUpdateStorageKey, -2); Log.d(TAG, "addToIndex - recreate: " + (lastDbUpdate > lastIndexUpdate)); if (lastDbUpdate > lastIndexUpdate) { Cursor cursor = null; try { String[] fields = new String[]{CollectionDb.TABLE_TRACKS + "." + CollectionDb.ID, CollectionDb.ARTISTS_ARTIST, CollectionDb.ALBUMS_ALBUM, CollectionDb.TRACKS_TRACK}; cursor = mCollectionDb.tracks(null, null, fields); beginIndexing(true); Log.d(TAG, "addToIndex - Adding tracks to index - count: " + cursor.getCount()); cursor.moveToFirst(); if (!cursor.isAfterLast()) { do { Document document = new Document(); document.add(new IntField("id", cursor.getInt(0), Field.Store.YES)); document.add(new StringField("artist", cursor.getString(1), Field.Store.YES)); document.add(new StringField("album", cursor.getString(2), Field.Store.YES)); document.add(new StringField("track", cursor.getString(3), Field.Store.YES)); mLuceneWriter.addDocument(document); } while (cursor.moveToNext()); } PreferenceUtils.edit().putLong(mLastUpdateStorageKey, System.currentTimeMillis()) .commit(); } catch (IOException e) { Log.e(TAG, "addToIndex - " + e.getClass() + ": " + e.getLocalizedMessage()); } finally { if (cursor != null) { cursor.close(); } endIndexing(); } } updateSearcherManager(); } private void updateSearcherManager() { Log.d(TAG, "updateSearcherManager"); try { if (mSearcherManager != null) { mSearcherManager.close(); } File indexDirFile = new File(mLucenePath); Directory dir = FSDirectory.open(indexDirFile); mSearcherManager = new SearcherManager(dir, new SearcherFactory()); } catch (IOException e) { Log.e(TAG, "updateSearcherManager - " + e.getClass() + ": " + e.getLocalizedMessage()); } } public void close() { Log.d(TAG, "close"); endIndexing(); if (mSearcherManager != null) { try { mSearcherManager.close(); } catch (IOException e) { Log.e(TAG, "close - " + e.getClass() + ": " + e.getLocalizedMessage()); } mSearcherManager = null; } } public synchronized List<IndexResult> searchIndex(Query query) { List<IndexResult> indexResults = new ArrayList<>(); try { BooleanQuery qry = new BooleanQuery(); if (query.isFullTextQuery()) { String escapedQuery = MultiFieldQueryParser.escape(query.getFullTextQuery()); Term term = new Term("track", escapedQuery); org.apache.lucene.search.Query fqry = new FuzzyQuery(term); qry.add(fqry, BooleanClause.Occur.SHOULD); term = new Term("artist", escapedQuery); fqry = new FuzzyQuery(term); qry.add(fqry, BooleanClause.Occur.SHOULD); term = new Term("fulltext", escapedQuery); fqry = new FuzzyQuery(term); qry.add(fqry, BooleanClause.Occur.SHOULD); Log.d(TAG, "searchIndex - fulltext: " + escapedQuery); } else { String escapedTrackName = MultiFieldQueryParser .escape(query.getBasicTrack().getName()); String escapedArtistName = MultiFieldQueryParser .escape(query.getArtist().getName()); Term term = new Term("track", escapedTrackName); org.apache.lucene.search.Query fqry = new FuzzyQuery(term); qry.add(fqry, BooleanClause.Occur.MUST); term = new Term("artist", escapedArtistName); fqry = new FuzzyQuery(term); qry.add(fqry, BooleanClause.Occur.MUST); Log.d(TAG, "searchIndex - non-fulltext: " + escapedArtistName + ", " + escapedTrackName); } IndexSearcher searcher = mSearcherManager.acquire(); long time = System.currentTimeMillis(); ScoreDoc[] hits = searcher.search(qry, 50).scoreDocs; Log.d(TAG, "searchIndex - searching took " + (System.currentTimeMillis() - time) + "ms"); for (ScoreDoc doc : hits) { Document document = searcher.doc(doc.doc); IndexResult indexResult = new IndexResult(); indexResult.id = document.getField("id").numericValue().intValue(); indexResult.score = doc.score; indexResults.add(indexResult); } mSearcherManager.release(searcher); } catch (IOException e) { Log.e(TAG, "searchIndex - " + e.getClass() + ": " + e.getLocalizedMessage()); } return indexResults; } /** * Initializes the IndexWriter to be able to add entries to the index. * * @param recreate whether or not to wipe any previously existing index */ private void beginIndexing(boolean recreate) throws IOException { Log.d(TAG, "beginIndexing - recreate: " + recreate); endIndexing(); File indexDirFile = new File(mLucenePath); Directory dir = FSDirectory.open(indexDirFile); Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_47); IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_47, analyzer); if (recreate) { PreferenceUtils.edit().putLong(mLastUpdateStorageKey, -2).commit(); iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE); } else { iwc.setOpenMode(IndexWriterConfig.OpenMode.APPEND); } mLuceneWriter = new IndexWriter(dir, iwc); } private void endIndexing() { Log.d(TAG, "endIndexing"); if (mLuceneWriter != null) { try { mLuceneWriter.commit(); mLuceneWriter.close(true); mLuceneWriter = null; } catch (IOException e) { Log.e(TAG, "endIndexing - " + e.getClass() + ": " + e.getLocalizedMessage()); } } } }