parent
42bced139f
commit
767b39ed5b
@ -0,0 +1,121 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import org.apache.lucene.analysis.ASCIIFoldingFilter; |
||||||
|
import org.apache.lucene.analysis.Analyzer; |
||||||
|
import org.apache.lucene.analysis.LowerCaseFilter; |
||||||
|
import org.apache.lucene.analysis.StopFilter; |
||||||
|
import org.apache.lucene.analysis.TokenStream; |
||||||
|
import org.apache.lucene.analysis.standard.StandardAnalyzer; |
||||||
|
import org.apache.lucene.analysis.standard.StandardFilter; |
||||||
|
import org.apache.lucene.analysis.standard.StandardTokenizer; |
||||||
|
import org.apache.lucene.util.Version; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.Reader; |
||||||
|
|
||||||
|
/** |
||||||
|
* Analyzer provider. |
||||||
|
* This class is a division of what was once part of SearchService and added functionality. |
||||||
|
* This class provides Analyzer which is used at index generation |
||||||
|
* and QueryAnalyzer which analyzes the specified query at search time. |
||||||
|
* Analyzer can be closed but is a reuse premise. |
||||||
|
* It is held in this class. |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public final class AnalyzerFactory { |
||||||
|
|
||||||
|
private Analyzer analyzer; |
||||||
|
|
||||||
|
private Analyzer queryAnalyzer; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return analyzer. |
||||||
|
* |
||||||
|
* @return analyzer for index |
||||||
|
*/ |
||||||
|
public Analyzer getAnalyzer() { |
||||||
|
if (null == analyzer) { |
||||||
|
analyzer = new CustomAnalyzer(); |
||||||
|
} |
||||||
|
return analyzer; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return analyzer. |
||||||
|
* |
||||||
|
* @return analyzer for index |
||||||
|
*/ |
||||||
|
public Analyzer getQueryAnalyzer() { |
||||||
|
if (null == queryAnalyzer) { |
||||||
|
queryAnalyzer = new CustomAnalyzer(); |
||||||
|
} |
||||||
|
return queryAnalyzer; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
* The legacy CustomAnalyzer implementation is kept as it is. |
||||||
|
*/ |
||||||
|
private class CustomAnalyzer extends StandardAnalyzer { |
||||||
|
private CustomAnalyzer() { |
||||||
|
/* |
||||||
|
* Version.LUCENE_30 |
||||||
|
* It is a transient description and will be deleted when upgrading the version. |
||||||
|
* SearchService variables are not used because the reference direction conflicts. |
||||||
|
*/ |
||||||
|
super(Version.LUCENE_30); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TokenStream tokenStream(String fieldName, Reader reader) { |
||||||
|
TokenStream result = super.tokenStream(fieldName, reader); |
||||||
|
return new ASCIIFoldingFilter(result); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException { |
||||||
|
class SavedStreams { |
||||||
|
StandardTokenizer tokenStream; |
||||||
|
TokenStream filteredTokenStream; |
||||||
|
} |
||||||
|
|
||||||
|
SavedStreams streams = (SavedStreams) getPreviousTokenStream(); |
||||||
|
if (streams == null) { |
||||||
|
streams = new SavedStreams(); |
||||||
|
setPreviousTokenStream(streams); |
||||||
|
streams.tokenStream = new StandardTokenizer(Version.LUCENE_30, reader); |
||||||
|
streams.filteredTokenStream = new StandardFilter(streams.tokenStream); |
||||||
|
streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream); |
||||||
|
streams.filteredTokenStream = new StopFilter(true, streams.filteredTokenStream, |
||||||
|
STOP_WORDS_SET); |
||||||
|
streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream); |
||||||
|
} else { |
||||||
|
streams.tokenStream.reset(reader); |
||||||
|
} |
||||||
|
streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH); |
||||||
|
|
||||||
|
return streams.filteredTokenStream; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,181 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.Album; |
||||||
|
import org.airsonic.player.domain.Artist; |
||||||
|
import org.airsonic.player.domain.MediaFile; |
||||||
|
import org.airsonic.player.domain.MusicFolder; |
||||||
|
import org.apache.lucene.document.Document; |
||||||
|
import org.apache.lucene.document.Field; |
||||||
|
import org.apache.lucene.document.NumericField; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
/** |
||||||
|
* A factory that generates the documents to be stored in the index. |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public class DocumentFactory { |
||||||
|
|
||||||
|
/** |
||||||
|
* Normalize the genre string. |
||||||
|
* |
||||||
|
* @param genre genre string |
||||||
|
* @return genre string normalized |
||||||
|
* @deprecated should be resolved with tokenizer or filter |
||||||
|
*/ |
||||||
|
@Deprecated |
||||||
|
private String normalizeGenre(String genre) { |
||||||
|
return genre.toLowerCase().replace(" ", "").replace("-", ""); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a document. |
||||||
|
* |
||||||
|
* @param mediaFile target of document |
||||||
|
* @return document |
||||||
|
* @since legacy |
||||||
|
*/ |
||||||
|
public Document createAlbumDocument(MediaFile mediaFile) { |
||||||
|
Document doc = new Document(); |
||||||
|
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
||||||
|
.setIntValue(mediaFile.getId())); |
||||||
|
|
||||||
|
if (mediaFile.getArtist() != null) { |
||||||
|
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (mediaFile.getAlbumName() != null) { |
||||||
|
doc.add(new Field(FieldNames.ALBUM, mediaFile.getAlbumName(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (mediaFile.getFolder() != null) { |
||||||
|
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, |
||||||
|
Field.Index.NOT_ANALYZED_NO_NORMS)); |
||||||
|
} |
||||||
|
return doc; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a document. |
||||||
|
* |
||||||
|
* @param mediaFile target of document |
||||||
|
* @return document |
||||||
|
* @since legacy |
||||||
|
*/ |
||||||
|
public Document createArtistDocument(MediaFile mediaFile) { |
||||||
|
Document doc = new Document(); |
||||||
|
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
||||||
|
.setIntValue(mediaFile.getId())); |
||||||
|
|
||||||
|
if (mediaFile.getArtist() != null) { |
||||||
|
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (mediaFile.getFolder() != null) { |
||||||
|
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, |
||||||
|
Field.Index.NOT_ANALYZED_NO_NORMS)); |
||||||
|
} |
||||||
|
return doc; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a document. |
||||||
|
* |
||||||
|
* @param album target of document |
||||||
|
* @return document |
||||||
|
* @since legacy |
||||||
|
*/ |
||||||
|
public Document createAlbumId3Document(Album album) { |
||||||
|
Document doc = new Document(); |
||||||
|
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false).setIntValue(album.getId())); |
||||||
|
|
||||||
|
if (album.getArtist() != null) { |
||||||
|
doc.add(new Field(FieldNames.ARTIST, album.getArtist(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (album.getName() != null) { |
||||||
|
doc.add(new Field(FieldNames.ALBUM, album.getName(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (album.getFolderId() != null) { |
||||||
|
doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true) |
||||||
|
.setIntValue(album.getFolderId())); |
||||||
|
} |
||||||
|
return doc; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a document. |
||||||
|
* |
||||||
|
* @param artist target of document |
||||||
|
* @param musicFolder target folder exists |
||||||
|
* @return document |
||||||
|
* @since legacy |
||||||
|
*/ |
||||||
|
public Document createArtistId3Document(Artist artist, MusicFolder musicFolder) { |
||||||
|
Document doc = new Document(); |
||||||
|
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
||||||
|
.setIntValue(artist.getId())); |
||||||
|
doc.add(new Field(FieldNames.ARTIST, artist.getName(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true) |
||||||
|
.setIntValue(musicFolder.getId())); |
||||||
|
return doc; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a document. |
||||||
|
* |
||||||
|
* @param mediaFile target of document |
||||||
|
* @return document |
||||||
|
* @since legacy |
||||||
|
*/ |
||||||
|
public Document createSongDocument(MediaFile mediaFile) { |
||||||
|
Document doc = new Document(); |
||||||
|
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false) |
||||||
|
.setIntValue(mediaFile.getId())); |
||||||
|
doc.add(new Field(FieldNames.MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO, |
||||||
|
Field.Index.ANALYZED_NO_NORMS)); |
||||||
|
if (mediaFile.getTitle() != null) { |
||||||
|
doc.add(new Field(FieldNames.TITLE, mediaFile.getTitle(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (mediaFile.getArtist() != null) { |
||||||
|
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES, |
||||||
|
Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (mediaFile.getGenre() != null) { |
||||||
|
doc.add(new Field(FieldNames.GENRE, normalizeGenre(mediaFile.getGenre()), |
||||||
|
Field.Store.NO, Field.Index.ANALYZED)); |
||||||
|
} |
||||||
|
if (mediaFile.getYear() != null) { |
||||||
|
doc.add(new NumericField(FieldNames.YEAR, Field.Store.NO, true) |
||||||
|
.setIntValue(mediaFile.getYear())); |
||||||
|
} |
||||||
|
if (mediaFile.getFolder() != null) { |
||||||
|
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO, |
||||||
|
Field.Index.NOT_ANALYZED_NO_NORMS)); |
||||||
|
} |
||||||
|
return doc; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
/** |
||||||
|
* Enum that symbolizes the field name used for lucene index. |
||||||
|
* This class is a division of what was once part of SearchService and added functionality. |
||||||
|
*/ |
||||||
|
class FieldNames { |
||||||
|
|
||||||
|
private FieldNames() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, id field. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String ID = "id"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, id field. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String FOLDER_ID = "folderId"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, numeric field. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String YEAR = "year"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, key field. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String GENRE = "genre"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, key field. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String MEDIA_TYPE = "mediaType"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, key field. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String FOLDER = "folder"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, usually with common word parsing. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String ARTIST = "artist"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, usually with common word parsing. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String ALBUM = "album"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A field same to a legacy server, usually with common word parsing. |
||||||
|
* |
||||||
|
* @since legacy |
||||||
|
**/ |
||||||
|
public static final String TITLE = "title"; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.Album; |
||||||
|
import org.airsonic.player.domain.Artist; |
||||||
|
import org.airsonic.player.domain.MediaFile; |
||||||
|
import org.airsonic.player.domain.MusicFolder; |
||||||
|
import org.airsonic.player.service.SettingsService; |
||||||
|
import org.airsonic.player.util.FileUtil; |
||||||
|
import org.apache.lucene.index.IndexReader; |
||||||
|
import org.apache.lucene.index.IndexWriter; |
||||||
|
import org.apache.lucene.store.FSDirectory; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
import static org.airsonic.player.service.search.IndexType.ALBUM; |
||||||
|
import static org.airsonic.player.service.search.IndexType.ALBUM_ID3; |
||||||
|
import static org.airsonic.player.service.search.IndexType.ARTIST; |
||||||
|
import static org.airsonic.player.service.search.IndexType.ARTIST_ID3; |
||||||
|
import static org.airsonic.player.service.search.IndexType.SONG; |
||||||
|
|
||||||
|
/** |
||||||
|
* Function class that is strongly linked to the lucene index implementation. |
||||||
|
* Legacy has an implementation in SearchService. |
||||||
|
* |
||||||
|
* If the index CRUD and search functionality are in the same class, |
||||||
|
* there is often a dependency conflict on the class used. |
||||||
|
* Although the interface of SearchService is left to maintain the legacy implementation, |
||||||
|
* it is desirable that methods of index operations other than search essentially use this class directly. |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public class IndexManager { |
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(IndexManager.class); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private AnalyzerFactory analyzerFactory; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private DocumentFactory documentFactory; |
||||||
|
|
||||||
|
private IndexWriter artistWriter; |
||||||
|
private IndexWriter artistId3Writer; |
||||||
|
private IndexWriter albumWriter; |
||||||
|
private IndexWriter albumId3Writer; |
||||||
|
private IndexWriter songWriter; |
||||||
|
|
||||||
|
public void index(Album album) { |
||||||
|
try { |
||||||
|
albumId3Writer.addDocument(documentFactory.createAlbumId3Document(album)); |
||||||
|
} catch (Exception x) { |
||||||
|
LOG.error("Failed to create search index for " + album, x); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void index(Artist artist, MusicFolder musicFolder) { |
||||||
|
try { |
||||||
|
artistId3Writer |
||||||
|
.addDocument(documentFactory.createArtistId3Document(artist, musicFolder)); |
||||||
|
} catch (Exception x) { |
||||||
|
LOG.error("Failed to create search index for " + artist, x); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void index(MediaFile mediaFile) { |
||||||
|
try { |
||||||
|
if (mediaFile.isFile()) { |
||||||
|
songWriter.addDocument(documentFactory.createSongDocument(mediaFile)); |
||||||
|
} else if (mediaFile.isAlbum()) { |
||||||
|
albumWriter.addDocument(documentFactory.createAlbumDocument(mediaFile)); |
||||||
|
} else { |
||||||
|
artistWriter.addDocument(documentFactory.createArtistDocument(mediaFile)); |
||||||
|
} |
||||||
|
} catch (Exception x) { |
||||||
|
LOG.error("Failed to create search index for " + mediaFile, x); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static final String LUCENE_DIR = "lucene2"; |
||||||
|
|
||||||
|
public IndexReader createIndexReader(IndexType indexType) throws IOException { |
||||||
|
File dir = getIndexDirectory(indexType); |
||||||
|
return IndexReader.open(FSDirectory.open(dir), true); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* It is static as an intermediate response of the transition period. |
||||||
|
* (It is called before injection because it is called by SearchService constructor) |
||||||
|
* |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
private static File getIndexRootDirectory() { |
||||||
|
return new File(SettingsService.getAirsonicHome(), LUCENE_DIR); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Make it public as an interim response of the transition period. |
||||||
|
* (It is called before the injection because it is called in the SearchService constructor.) |
||||||
|
* |
||||||
|
* @param indexType |
||||||
|
* @return |
||||||
|
* @deprecated It should not be called from outside. |
||||||
|
*/ |
||||||
|
@Deprecated |
||||||
|
public static File getIndexDirectory(IndexType indexType) { |
||||||
|
return new File(getIndexRootDirectory(), indexType.toString().toLowerCase()); |
||||||
|
} |
||||||
|
|
||||||
|
private IndexWriter createIndexWriter(IndexType indexType) throws IOException { |
||||||
|
File dir = getIndexDirectory(indexType); |
||||||
|
return new IndexWriter(FSDirectory.open(dir), analyzerFactory.getAnalyzer(), true, |
||||||
|
new IndexWriter.MaxFieldLength(10)); |
||||||
|
} |
||||||
|
|
||||||
|
public final void startIndexing() { |
||||||
|
try { |
||||||
|
artistWriter = createIndexWriter(ARTIST); |
||||||
|
artistId3Writer = createIndexWriter(ARTIST_ID3); |
||||||
|
albumWriter = createIndexWriter(ALBUM); |
||||||
|
albumId3Writer = createIndexWriter(ALBUM_ID3); |
||||||
|
songWriter = createIndexWriter(SONG); |
||||||
|
} catch (Exception x) { |
||||||
|
LOG.error("Failed to create search index.", x); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void stopIndexing() { |
||||||
|
try { |
||||||
|
artistWriter.optimize(); |
||||||
|
artistId3Writer.optimize(); |
||||||
|
albumWriter.optimize(); |
||||||
|
albumId3Writer.optimize(); |
||||||
|
songWriter.optimize(); |
||||||
|
} catch (Exception x) { |
||||||
|
LOG.error("Failed to create search index.", x); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(artistId3Writer); |
||||||
|
FileUtil.closeQuietly(artistWriter); |
||||||
|
FileUtil.closeQuietly(albumWriter); |
||||||
|
FileUtil.closeQuietly(albumId3Writer); |
||||||
|
FileUtil.closeQuietly(songWriter); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,231 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.MediaFile; |
||||||
|
import org.airsonic.player.domain.MusicFolder; |
||||||
|
import org.airsonic.player.domain.RandomSearchCriteria; |
||||||
|
import org.airsonic.player.domain.SearchCriteria; |
||||||
|
import org.apache.lucene.analysis.ASCIIFoldingFilter; |
||||||
|
import org.apache.lucene.analysis.standard.StandardTokenizer; |
||||||
|
import org.apache.lucene.analysis.tokenattributes.TermAttribute; |
||||||
|
import org.apache.lucene.index.Term; |
||||||
|
import org.apache.lucene.queryParser.MultiFieldQueryParser; |
||||||
|
import org.apache.lucene.queryParser.ParseException; |
||||||
|
import org.apache.lucene.queryParser.QueryParser; |
||||||
|
import org.apache.lucene.search.BooleanClause; |
||||||
|
import org.apache.lucene.search.BooleanQuery; |
||||||
|
import org.apache.lucene.search.NumericRangeQuery; |
||||||
|
import org.apache.lucene.search.Query; |
||||||
|
import org.apache.lucene.search.TermQuery; |
||||||
|
import org.apache.lucene.search.spans.SpanOrQuery; |
||||||
|
import org.apache.lucene.search.spans.SpanQuery; |
||||||
|
import org.apache.lucene.search.spans.SpanTermQuery; |
||||||
|
import org.apache.lucene.util.NumericUtils; |
||||||
|
import org.apache.lucene.util.Version; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.StringReader; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static org.airsonic.player.service.search.IndexType.ALBUM_ID3; |
||||||
|
import static org.airsonic.player.service.search.IndexType.ARTIST_ID3; |
||||||
|
|
||||||
|
/** |
||||||
|
* Factory class of Lucene Query. |
||||||
|
* This class is an extract of the functionality that was once part of SearchService. |
||||||
|
* It is for maintainability and verification. |
||||||
|
* Each corresponds to the SearchService method. |
||||||
|
* The API syntax for query generation depends on the lucene version. |
||||||
|
* verification with query grammar is possible. |
||||||
|
* On the other hand, the generated queries are relatively small by version. |
||||||
|
* Therefore, test cases of this class are useful for large version upgrades. |
||||||
|
**/ |
||||||
|
@Component |
||||||
|
public class QueryFactory { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private AnalyzerFactory analyzerFactory; |
||||||
|
|
||||||
|
private String analyzeQuery(String query) throws IOException { |
||||||
|
StringBuilder result = new StringBuilder(); |
||||||
|
/* |
||||||
|
* Version.LUCENE_30 |
||||||
|
* It is a transient description and will be deleted when upgrading the version. |
||||||
|
* SearchService variables are not used because the reference direction conflicts. |
||||||
|
*/ |
||||||
|
@SuppressWarnings("resource") |
||||||
|
ASCIIFoldingFilter filter = new ASCIIFoldingFilter( |
||||||
|
new StandardTokenizer(Version.LUCENE_30, new StringReader(query))); |
||||||
|
TermAttribute termAttribute = filter.getAttribute(TermAttribute.class); |
||||||
|
while (filter.incrementToken()) { |
||||||
|
result.append(termAttribute.term()).append("* "); |
||||||
|
} |
||||||
|
return result.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Normalize the genre string. |
||||||
|
* |
||||||
|
* @param genre genre string |
||||||
|
* @return genre string normalized |
||||||
|
* @deprecated should be resolved with tokenizer or filter |
||||||
|
*/ |
||||||
|
@Deprecated |
||||||
|
private String normalizeGenre(String genre) { |
||||||
|
return genre.toLowerCase().replace(" ", "").replace("-", ""); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Query generation expression extracted from |
||||||
|
* {@link org.airsonic.player.service.SearchService#search(SearchCriteria, List, IndexType)}. |
||||||
|
* |
||||||
|
* @param criteria criteria |
||||||
|
* @param musicFolders musicFolders |
||||||
|
* @param indexType {@link IndexType} |
||||||
|
* @return Query |
||||||
|
* @throws IOException When parsing of MultiFieldQueryParser fails |
||||||
|
* @throws ParseException When parsing of MultiFieldQueryParser fails |
||||||
|
*/ |
||||||
|
public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders, |
||||||
|
IndexType indexType) throws ParseException, IOException { |
||||||
|
/* |
||||||
|
* Version.LUCENE_30 |
||||||
|
* It is a transient description and will be deleted when upgrading the version. |
||||||
|
* SearchService variables are not used because the reference direction conflicts. |
||||||
|
*/ |
||||||
|
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_30, |
||||||
|
indexType.getFields(), analyzerFactory.getQueryAnalyzer(), indexType.getBoosts()); |
||||||
|
|
||||||
|
BooleanQuery query = new BooleanQuery(); |
||||||
|
query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST); |
||||||
|
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
||||||
|
for (MusicFolder musicFolder : musicFolders) { |
||||||
|
if (indexType == ALBUM_ID3 || indexType == ARTIST_ID3) { |
||||||
|
musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID, |
||||||
|
NumericUtils.intToPrefixCoded(musicFolder.getId())))); |
||||||
|
} else { |
||||||
|
musicFolderQueries.add(new SpanTermQuery( |
||||||
|
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); |
||||||
|
} |
||||||
|
} |
||||||
|
query.add( |
||||||
|
new SpanOrQuery( |
||||||
|
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), |
||||||
|
BooleanClause.Occur.MUST); |
||||||
|
return query; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Query generation expression extracted from |
||||||
|
* {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}. |
||||||
|
* |
||||||
|
* @param criteria criteria |
||||||
|
* @return Query |
||||||
|
*/ |
||||||
|
public Query getRandomSongs(RandomSearchCriteria criteria) { |
||||||
|
BooleanQuery query = new BooleanQuery(); |
||||||
|
query.add(new TermQuery( |
||||||
|
new Term(FieldNames.MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())), |
||||||
|
BooleanClause.Occur.MUST); |
||||||
|
if (criteria.getGenre() != null) { |
||||||
|
String genre = normalizeGenre(criteria.getGenre()); |
||||||
|
query.add(new TermQuery(new Term(FieldNames.GENRE, genre)), BooleanClause.Occur.MUST); |
||||||
|
} |
||||||
|
if (criteria.getFromYear() != null || criteria.getToYear() != null) { |
||||||
|
NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FieldNames.YEAR, |
||||||
|
criteria.getFromYear(), criteria.getToYear(), true, true); |
||||||
|
query.add(rangeQuery, BooleanClause.Occur.MUST); |
||||||
|
} |
||||||
|
|
||||||
|
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
||||||
|
for (MusicFolder musicFolder : criteria.getMusicFolders()) { |
||||||
|
musicFolderQueries.add(new SpanTermQuery( |
||||||
|
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); |
||||||
|
} |
||||||
|
query.add( |
||||||
|
new SpanOrQuery( |
||||||
|
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), |
||||||
|
BooleanClause.Occur.MUST); |
||||||
|
return query; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Query generation expression extracted from |
||||||
|
* {@link org.airsonic.player.service.SearchService#searchByName( String, String, int, int, List, Class)}. |
||||||
|
* |
||||||
|
* @param fieldName {@link FieldNames} |
||||||
|
* @return Query |
||||||
|
* @throws ParseException When parsing of QueryParser fails |
||||||
|
*/ |
||||||
|
public Query searchByName(String fieldName, String name) throws ParseException { |
||||||
|
/* |
||||||
|
* Version.LUCENE_30 |
||||||
|
* It is a transient description and will be deleted when upgrading the version. |
||||||
|
* SearchService variables are not used because the reference direction conflicts. |
||||||
|
*/ |
||||||
|
QueryParser queryParser = new QueryParser(Version.LUCENE_30, fieldName, |
||||||
|
analyzerFactory.getQueryAnalyzer()); |
||||||
|
Query query = queryParser.parse(name + "*"); |
||||||
|
return query; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Query generation expression extracted from |
||||||
|
* {@link org.airsonic.player.service.SearchService#getRandomAlbums(int, List)}. |
||||||
|
* |
||||||
|
* @param musicFolders musicFolders |
||||||
|
* @return Query |
||||||
|
*/ |
||||||
|
public Query getRandomAlbums(List<MusicFolder> musicFolders) { |
||||||
|
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
||||||
|
for (MusicFolder musicFolder : musicFolders) { |
||||||
|
musicFolderQueries.add(new SpanTermQuery( |
||||||
|
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath()))); |
||||||
|
} |
||||||
|
Query query = new SpanOrQuery( |
||||||
|
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); |
||||||
|
return query; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Query generation expression extracted from |
||||||
|
* {@link org.airsonic.player.service.SearchService#getRandomAlbumsId3(int, List)}. |
||||||
|
* |
||||||
|
* @param musicFolders musicFolders |
||||||
|
* @return Query |
||||||
|
*/ |
||||||
|
public Query getRandomAlbumsId3(List<MusicFolder> musicFolders) { |
||||||
|
|
||||||
|
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>(); |
||||||
|
for (MusicFolder musicFolder : musicFolders) { |
||||||
|
musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID, |
||||||
|
NumericUtils.intToPrefixCoded(musicFolder.getId())))); |
||||||
|
} |
||||||
|
Query query = new SpanOrQuery( |
||||||
|
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); |
||||||
|
return query; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,298 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.*; |
||||||
|
import org.airsonic.player.service.SearchService; |
||||||
|
import org.airsonic.player.util.FileUtil; |
||||||
|
import org.apache.lucene.document.Document; |
||||||
|
import org.apache.lucene.index.IndexReader; |
||||||
|
import org.apache.lucene.index.IndexWriter; |
||||||
|
import org.apache.lucene.queryParser.ParseException; |
||||||
|
import org.apache.lucene.search.*; |
||||||
|
import org.apache.lucene.store.Directory; |
||||||
|
import org.apache.lucene.store.FSDirectory; |
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Service; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.*; |
||||||
|
import java.util.function.BiConsumer; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import static org.airsonic.player.service.search.IndexType.*; |
||||||
|
import static org.springframework.util.ObjectUtils.isEmpty; |
||||||
|
|
||||||
|
@Service |
||||||
|
public class SearchServiceImpl implements SearchService { |
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SearchServiceImpl.class); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private QueryFactory queryFactory; |
||||||
|
@Autowired |
||||||
|
private IndexManager indexManager; |
||||||
|
@Autowired |
||||||
|
private SearchServiceUtilities util; |
||||||
|
|
||||||
|
// TODO Should be changed to SecureRandom?
|
||||||
|
private final Random random = new Random(System.currentTimeMillis()); |
||||||
|
|
||||||
|
public SearchServiceImpl() { |
||||||
|
removeLocks(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void startIndexing() { |
||||||
|
indexManager.startIndexing(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void index(MediaFile mediaFile) { |
||||||
|
indexManager.index(mediaFile); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void index(Artist artist, MusicFolder musicFolder) { |
||||||
|
indexManager.index(artist, musicFolder); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void index(Album album) { |
||||||
|
indexManager.index(album); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void stopIndexing() { |
||||||
|
indexManager.stopIndexing(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders, |
||||||
|
IndexType indexType) { |
||||||
|
|
||||||
|
SearchResult result = new SearchResult(); |
||||||
|
int offset = criteria.getOffset(); |
||||||
|
int count = criteria.getCount(); |
||||||
|
result.setOffset(offset); |
||||||
|
|
||||||
|
if (count <= 0) |
||||||
|
return result; |
||||||
|
|
||||||
|
IndexReader reader = null; |
||||||
|
try { |
||||||
|
|
||||||
|
reader = indexManager.createIndexReader(indexType); |
||||||
|
Searcher searcher = new IndexSearcher(reader); |
||||||
|
Query query = queryFactory.search(criteria, musicFolders, indexType); |
||||||
|
|
||||||
|
TopDocs topDocs = searcher.search(query, null, offset + count); |
||||||
|
result.setTotalHits(topDocs.totalHits); |
||||||
|
int start = Math.min(offset, topDocs.totalHits); |
||||||
|
int end = Math.min(start + count, topDocs.totalHits); |
||||||
|
|
||||||
|
for (int i = start; i < end; i++) { |
||||||
|
util.addIfAnyMatch(result, indexType, searcher.doc(topDocs.scoreDocs[i].doc)); |
||||||
|
} |
||||||
|
|
||||||
|
} catch (IOException | ParseException e) { |
||||||
|
LOG.error("Failed to execute Lucene search.", e); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(reader); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Common processing of random method. |
||||||
|
* |
||||||
|
* @param count Number of albums to return. |
||||||
|
* @param searcher |
||||||
|
* @param query |
||||||
|
* @param id2ListCallBack Callback to get D from id and store it in List |
||||||
|
* @return result |
||||||
|
* @throws IOException |
||||||
|
*/ |
||||||
|
private final <D> List<D> createRandomDocsList( |
||||||
|
int count, Searcher searcher, Query query, BiConsumer<List<D>, Integer> id2ListCallBack) |
||||||
|
throws IOException { |
||||||
|
|
||||||
|
List<Integer> docs = Arrays |
||||||
|
.stream(searcher.search(query, Integer.MAX_VALUE).scoreDocs) |
||||||
|
.map(sd -> sd.doc) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
|
||||||
|
List<D> result = new ArrayList<>(); |
||||||
|
while (!docs.isEmpty() && result.size() < count) { |
||||||
|
int randomPos = random.nextInt(docs.size()); |
||||||
|
Document document = searcher.doc(docs.get(randomPos)); |
||||||
|
id2ListCallBack.accept(result, util.getId.apply(document)); |
||||||
|
docs.remove(randomPos); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) { |
||||||
|
|
||||||
|
IndexReader reader = null; |
||||||
|
try { |
||||||
|
reader = indexManager.createIndexReader(SONG); |
||||||
|
Searcher searcher = new IndexSearcher(reader); |
||||||
|
if (isEmpty(searcher)) { |
||||||
|
// At first start
|
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
Query query = queryFactory.getRandomSongs(criteria); |
||||||
|
return createRandomDocsList(criteria.getCount(), searcher, query, |
||||||
|
(dist, id) -> util.addIgnoreNull(dist, SONG, id)); |
||||||
|
|
||||||
|
} catch (IOException e) { |
||||||
|
LOG.error("Failed to search or random songs.", e); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(reader); |
||||||
|
} |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) { |
||||||
|
|
||||||
|
IndexReader reader = null; |
||||||
|
try { |
||||||
|
reader = indexManager.createIndexReader(ALBUM); |
||||||
|
Searcher searcher = new IndexSearcher(reader); |
||||||
|
if (isEmpty(searcher)) { |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
Query query = queryFactory.getRandomAlbums(musicFolders); |
||||||
|
return createRandomDocsList(count, searcher, query, |
||||||
|
(dist, id) -> util.addIgnoreNull(dist, ALBUM, id)); |
||||||
|
|
||||||
|
} catch (IOException e) { |
||||||
|
LOG.error("Failed to search for random albums.", e); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(reader); |
||||||
|
} |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) { |
||||||
|
|
||||||
|
IndexReader reader = null; |
||||||
|
try { |
||||||
|
reader = indexManager.createIndexReader(ALBUM_ID3); |
||||||
|
Searcher searcher = new IndexSearcher(reader); |
||||||
|
if (isEmpty(searcher)) { |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
Query query = queryFactory.getRandomAlbumsId3(musicFolders); |
||||||
|
return createRandomDocsList(count, searcher, query, |
||||||
|
(dist, id) -> util.addIgnoreNull(dist, ALBUM_ID3, id)); |
||||||
|
|
||||||
|
} catch (IOException e) { |
||||||
|
LOG.error("Failed to search for random albums.", e); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(reader); |
||||||
|
} |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public <T> ParamSearchResult<T> searchByName(String name, int offset, int count, |
||||||
|
List<MusicFolder> folderList, Class<T> assignableClass) { |
||||||
|
|
||||||
|
// we only support album, artist, and song for now
|
||||||
|
@Nullable |
||||||
|
IndexType indexType = util.getIndexType.apply(assignableClass); |
||||||
|
@Nullable |
||||||
|
String fieldName = util.getFieldName.apply(assignableClass); |
||||||
|
|
||||||
|
ParamSearchResult<T> result = new ParamSearchResult<T>(); |
||||||
|
result.setOffset(offset); |
||||||
|
|
||||||
|
if (isEmpty(indexType) || isEmpty(fieldName) || count <= 0) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
IndexReader reader = null; |
||||||
|
|
||||||
|
try { |
||||||
|
reader = indexManager.createIndexReader(indexType); |
||||||
|
Searcher searcher = new IndexSearcher(reader); |
||||||
|
Query query = queryFactory.searchByName(fieldName, name); |
||||||
|
|
||||||
|
Sort sort = new Sort(new SortField(fieldName, SortField.STRING)); |
||||||
|
TopDocs topDocs = searcher.search(query, null, offset + count, sort); |
||||||
|
|
||||||
|
result.setTotalHits(topDocs.totalHits); |
||||||
|
int start = Math.min(offset, topDocs.totalHits); |
||||||
|
int end = Math.min(start + count, topDocs.totalHits); |
||||||
|
|
||||||
|
for (int i = start; i < end; i++) { |
||||||
|
Document doc = searcher.doc(topDocs.scoreDocs[i].doc); |
||||||
|
util.addIgnoreNull(result, indexType, util.getId.apply(doc), assignableClass); |
||||||
|
} |
||||||
|
|
||||||
|
} catch (IOException | ParseException e) { |
||||||
|
LOG.error("Failed to execute Lucene search.", e); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(reader); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Locks are managed automatically by the framework. |
||||||
|
* |
||||||
|
* @deprecated It becomes unnecessary at the time of version upgrade. |
||||||
|
*/ |
||||||
|
@Deprecated |
||||||
|
public void removeLocks() { |
||||||
|
for (IndexType indexType : IndexType.values()) { |
||||||
|
Directory dir = null; |
||||||
|
try { |
||||||
|
/* |
||||||
|
* Static access to the accompanying method is performed as a transition period. |
||||||
|
* (Unnecessary processing after updating Lucene.) |
||||||
|
*/ |
||||||
|
dir = FSDirectory.open(IndexManager.getIndexDirectory(indexType)); |
||||||
|
if (IndexWriter.isLocked(dir)) { |
||||||
|
IndexWriter.unlock(dir); |
||||||
|
LOG.info("Removed Lucene lock file in " + dir); |
||||||
|
} |
||||||
|
} catch (Exception x) { |
||||||
|
LOG.warn("Failed to remove Lucene lock file in " + dir, x); |
||||||
|
} finally { |
||||||
|
FileUtil.closeQuietly(dir); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,200 @@ |
|||||||
|
/* |
||||||
|
This file is part of Airsonic. |
||||||
|
|
||||||
|
Airsonic 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. |
||||||
|
|
||||||
|
Airsonic 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 Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Copyright 2016 (C) Airsonic Authors |
||||||
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import org.airsonic.player.dao.AlbumDao; |
||||||
|
import org.airsonic.player.dao.ArtistDao; |
||||||
|
import org.airsonic.player.domain.Album; |
||||||
|
import org.airsonic.player.domain.Artist; |
||||||
|
import org.airsonic.player.domain.MediaFile; |
||||||
|
import org.airsonic.player.domain.ParamSearchResult; |
||||||
|
import org.airsonic.player.domain.SearchResult; |
||||||
|
import org.airsonic.player.service.MediaFileService; |
||||||
|
import org.airsonic.player.service.SettingsService; |
||||||
|
import org.apache.commons.collections.CollectionUtils; |
||||||
|
import org.apache.lucene.document.Document; |
||||||
|
import org.apache.lucene.index.Term; |
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.List; |
||||||
|
import java.util.function.BiConsumer; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Function; |
||||||
|
|
||||||
|
import static org.springframework.util.ObjectUtils.isEmpty; |
||||||
|
|
||||||
|
/** |
||||||
|
* Termination used by SearchService. |
||||||
|
* |
||||||
|
* Since SearchService operates as a proxy for storage (DB) using lucene, |
||||||
|
* there are many redundant descriptions different from essential data processing. |
||||||
|
* This class is a transfer class for saving those redundant descriptions. |
||||||
|
* |
||||||
|
* Exception handling is not termination, |
||||||
|
* so do not include exception handling in this class. |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
public class SearchServiceUtilities { |
||||||
|
|
||||||
|
/* Search by id only. */ |
||||||
|
@Autowired |
||||||
|
private ArtistDao artistDao; |
||||||
|
|
||||||
|
/* Search by id only. */ |
||||||
|
@Autowired |
||||||
|
private AlbumDao albumDao; |
||||||
|
|
||||||
|
/* |
||||||
|
* Search by id only. |
||||||
|
* Although there is no influence at present, |
||||||
|
* mediaFileService has a caching mechanism. |
||||||
|
* Service is used instead of Dao until you are sure you need to use mediaFileDao. |
||||||
|
*/ |
||||||
|
@Autowired |
||||||
|
private MediaFileService mediaFileService; |
||||||
|
|
||||||
|
public final Function<Long, Integer> round = (i) -> { |
||||||
|
// return
|
||||||
|
// NumericUtils.floatToSortableInt(i);
|
||||||
|
return i.intValue(); |
||||||
|
}; |
||||||
|
|
||||||
|
public final Function<Document, Integer> getId = d -> { |
||||||
|
return Integer.valueOf(d.get(FieldNames.ID)); |
||||||
|
}; |
||||||
|
|
||||||
|
public final BiConsumer<List<MediaFile>, Integer> addMediaFileIfAnyMatch = (dist, id) -> { |
||||||
|
if (!dist.stream().anyMatch(m -> id == m.getId())) { |
||||||
|
MediaFile mediaFile = mediaFileService.getMediaFile(id); |
||||||
|
if (!isEmpty(mediaFile)) { |
||||||
|
dist.add(mediaFile); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public final BiConsumer<List<Artist>, Integer> addArtistId3IfAnyMatch = (dist, id) -> { |
||||||
|
if (!dist.stream().anyMatch(a -> id == a.getId())) { |
||||||
|
Artist artist = artistDao.getArtist(id); |
||||||
|
if (!isEmpty(artist)) { |
||||||
|
dist.add(artist); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public final Function<Class<?>, @Nullable IndexType> getIndexType = (assignableClass) -> { |
||||||
|
IndexType indexType = null; |
||||||
|
if (assignableClass.isAssignableFrom(Album.class)) { |
||||||
|
indexType = IndexType.ALBUM_ID3; |
||||||
|
} else if (assignableClass.isAssignableFrom(Artist.class)) { |
||||||
|
indexType = IndexType.ARTIST_ID3; |
||||||
|
} else if (assignableClass.isAssignableFrom(MediaFile.class)) { |
||||||
|
indexType = IndexType.SONG; |
||||||
|
} |
||||||
|
return indexType; |
||||||
|
}; |
||||||
|
|
||||||
|
public final Function<Class<?>, @Nullable String> getFieldName = (assignableClass) -> { |
||||||
|
String fieldName = null; |
||||||
|
if (assignableClass.isAssignableFrom(Album.class)) { |
||||||
|
fieldName = FieldNames.ALBUM; |
||||||
|
} else if (assignableClass.isAssignableFrom(Artist.class)) { |
||||||
|
fieldName = FieldNames.ARTIST; |
||||||
|
} else if (assignableClass.isAssignableFrom(MediaFile.class)) { |
||||||
|
fieldName = FieldNames.TITLE; |
||||||
|
} |
||||||
|
return fieldName; |
||||||
|
}; |
||||||
|
|
||||||
|
public final BiConsumer<List<Album>, Integer> addAlbumId3IfAnyMatch = (dist, subjectId) -> { |
||||||
|
if (!dist.stream().anyMatch(a -> subjectId == a.getId())) { |
||||||
|
Album album = albumDao.getAlbum(subjectId); |
||||||
|
if (!isEmpty(album)) { |
||||||
|
dist.add(album); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private final Function<String, File> getRootDirectory = (version) -> { |
||||||
|
return new File(SettingsService.getAirsonicHome(), version); |
||||||
|
}; |
||||||
|
|
||||||
|
public final BiFunction<String, IndexType, File> getDirectory = (version, indexType) -> { |
||||||
|
return new File(getRootDirectory.apply(version), indexType.toString().toLowerCase()); |
||||||
|
}; |
||||||
|
|
||||||
|
public final Term createPrimarykey(Album album) { |
||||||
|
return new Term(FieldNames.ID, Integer.toString(album.getId())); |
||||||
|
}; |
||||||
|
|
||||||
|
public final Term createPrimarykey(Artist artist) { |
||||||
|
return new Term(FieldNames.ID, Integer.toString(artist.getId())); |
||||||
|
}; |
||||||
|
|
||||||
|
public final Term createPrimarykey(MediaFile mediaFile) { |
||||||
|
return new Term(FieldNames.ID, Integer.toString(mediaFile.getId())); |
||||||
|
}; |
||||||
|
|
||||||
|
public final boolean addIgnoreNull(Collection<?> collection, Object object) { |
||||||
|
return CollectionUtils.addIgnoreNull(collection, object); |
||||||
|
} |
||||||
|
|
||||||
|
public final boolean addIgnoreNull(Collection<?> collection, IndexType indexType, |
||||||
|
int subjectId) { |
||||||
|
if (indexType == IndexType.ALBUM | indexType == IndexType.SONG) { |
||||||
|
return addIgnoreNull(collection, mediaFileService.getMediaFile(subjectId)); |
||||||
|
} else if (indexType == IndexType.ALBUM_ID3) { |
||||||
|
return addIgnoreNull(collection, albumDao.getAlbum(subjectId)); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public final <T> void addIgnoreNull(ParamSearchResult<T> dist, IndexType indexType, |
||||||
|
int subjectId, Class<T> subjectClass) { |
||||||
|
if (indexType == IndexType.SONG) { |
||||||
|
MediaFile mediaFile = mediaFileService.getMediaFile(subjectId); |
||||||
|
addIgnoreNull(dist.getItems(), subjectClass.cast(mediaFile)); |
||||||
|
} else if (indexType == IndexType.ARTIST_ID3) { |
||||||
|
Artist artist = artistDao.getArtist(subjectId); |
||||||
|
addIgnoreNull(dist.getItems(), subjectClass.cast(artist)); |
||||||
|
} else if (indexType == IndexType.ALBUM_ID3) { |
||||||
|
Album album = albumDao.getAlbum(subjectId); |
||||||
|
addIgnoreNull(dist.getItems(), subjectClass.cast(album)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public final void addIfAnyMatch(SearchResult dist, IndexType subjectIndexType, |
||||||
|
Document subject) { |
||||||
|
int documentId = getId.apply(subject); |
||||||
|
if (subjectIndexType == IndexType.ARTIST | subjectIndexType == IndexType.ALBUM |
||||||
|
| subjectIndexType == IndexType.SONG) { |
||||||
|
addMediaFileIfAnyMatch.accept(dist.getMediaFiles(), documentId); |
||||||
|
} else if (subjectIndexType == IndexType.ARTIST_ID3) { |
||||||
|
addArtistId3IfAnyMatch.accept(dist.getArtists(), documentId); |
||||||
|
} else if (subjectIndexType == IndexType.ALBUM_ID3) { |
||||||
|
addAlbumId3IfAnyMatch.accept(dist.getAlbums(), documentId); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,256 @@ |
|||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
|
||||||
|
import org.airsonic.player.domain.MusicFolder; |
||||||
|
import org.airsonic.player.domain.RandomSearchCriteria; |
||||||
|
import org.airsonic.player.domain.SearchCriteria; |
||||||
|
import org.airsonic.player.util.HomeRule; |
||||||
|
import org.apache.commons.lang.builder.ToStringBuilder; |
||||||
|
import org.apache.lucene.queryParser.ParseException; |
||||||
|
import org.apache.lucene.search.Query; |
||||||
|
import org.apache.lucene.util.NumericUtils; |
||||||
|
import org.junit.ClassRule; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.Description; |
||||||
|
import org.junit.runners.model.Statement; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.test.annotation.DirtiesContext; |
||||||
|
import org.springframework.test.context.ContextConfiguration; |
||||||
|
import org.springframework.test.context.junit4.rules.SpringClassRule; |
||||||
|
import org.springframework.test.context.junit4.rules.SpringMethodRule; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* Test case for QueryFactory. |
||||||
|
* These cases have the purpose of observing the current situation |
||||||
|
* and observing the impact of upgrading Lucene. |
||||||
|
*/ |
||||||
|
@ContextConfiguration( |
||||||
|
locations = { |
||||||
|
"/applicationContext-service.xml", |
||||||
|
"/applicationContext-cache.xml", |
||||||
|
"/applicationContext-testdb.xml", |
||||||
|
"/applicationContext-mockSonos.xml" }) |
||||||
|
@DirtiesContext( |
||||||
|
classMode = DirtiesContext.ClassMode.AFTER_CLASS) |
||||||
|
public class QueryFactoryTestCase { |
||||||
|
|
||||||
|
@ClassRule |
||||||
|
public static final SpringClassRule classRule = new SpringClassRule() { |
||||||
|
HomeRule homeRule = new HomeRule(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public Statement apply(Statement base, Description description) { |
||||||
|
Statement spring = super.apply(base, description); |
||||||
|
return homeRule.apply(spring, description); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
@Rule |
||||||
|
public final SpringMethodRule springMethodRule = new SpringMethodRule(); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private QueryFactory queryFactory; |
||||||
|
|
||||||
|
private static final String QUERY_ENG_ONLY = "ABC DEF"; |
||||||
|
|
||||||
|
private static final String SEPA = System.getProperty("file.separator"); |
||||||
|
|
||||||
|
private static final String PATH1 = SEPA + "var" + SEPA + "music1"; |
||||||
|
private static final String PATH2 = SEPA + "var" + SEPA + "music2"; |
||||||
|
|
||||||
|
private static final int FID1 = 10; |
||||||
|
private static final int FID2 = 20; |
||||||
|
|
||||||
|
private static final MusicFolder MUSIC_FOLDER1 = |
||||||
|
new MusicFolder(Integer.valueOf(FID1), new File(PATH1), "music1", true, new java.util.Date()); |
||||||
|
private static final MusicFolder MUSIC_FOLDER2 = |
||||||
|
new MusicFolder(Integer.valueOf(FID2), new File(PATH2), "music2", true, new java.util.Date()); |
||||||
|
|
||||||
|
private static final List<MusicFolder> SINGLE_FOLDERS = Arrays.asList(MUSIC_FOLDER1); |
||||||
|
private static final List<MusicFolder> MULTI_FOLDERS = Arrays.asList(MUSIC_FOLDER1, MUSIC_FOLDER2); |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchArtist() throws ParseException, IOException { |
||||||
|
SearchCriteria criteria = new SearchCriteria(); |
||||||
|
criteria.setOffset(10); |
||||||
|
criteria.setCount(Integer.MAX_VALUE); |
||||||
|
criteria.setQuery(QUERY_ENG_ONLY); |
||||||
|
|
||||||
|
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST); |
||||||
|
assertEquals("SearchArtist", |
||||||
|
"+((artist:abc* folder:abc*) (artist:def* folder:def*)) +spanOr([folder:" + PATH1 + "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ARTIST); |
||||||
|
assertEquals("SearchArtist", "+((artist:abc* folder:abc*) (artist:def* folder:def*)) +spanOr([folder:" + PATH1 |
||||||
|
+ ", folder:" + PATH2 + "])", query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchAlbum() throws ParseException, IOException { |
||||||
|
SearchCriteria criteria = new SearchCriteria(); |
||||||
|
criteria.setOffset(10); |
||||||
|
criteria.setCount(Integer.MAX_VALUE); |
||||||
|
criteria.setQuery(QUERY_ENG_ONLY); |
||||||
|
|
||||||
|
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ALBUM); |
||||||
|
assertEquals("SearchAlbum", |
||||||
|
"+((album:abc* artist:abc* folder:abc*) (album:def* artist:def* folder:def*)) +spanOr([folder:" + PATH1 |
||||||
|
+ "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ALBUM); |
||||||
|
assertEquals("SearchAlbum", |
||||||
|
"+((album:abc* artist:abc* folder:abc*) (album:def* artist:def* folder:def*)) +spanOr([folder:" + PATH1 |
||||||
|
+ ", folder:" + PATH2 + "])", |
||||||
|
query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchSong() throws ParseException, IOException { |
||||||
|
SearchCriteria criteria = new SearchCriteria(); |
||||||
|
criteria.setOffset(10); |
||||||
|
criteria.setCount(Integer.MAX_VALUE); |
||||||
|
criteria.setQuery(QUERY_ENG_ONLY); |
||||||
|
|
||||||
|
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.SONG); |
||||||
|
assertEquals("SearchSong", |
||||||
|
"+((title:abc* artist:abc*) (title:def* artist:def*)) +spanOr([folder:" + PATH1 + "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.SONG); |
||||||
|
assertEquals("SearchSong", "+((title:abc* artist:abc*) (title:def* artist:def*)) +spanOr([folder:" + PATH1 |
||||||
|
+ ", folder:" + PATH2 + "])", query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchArtistId3() throws ParseException, IOException { |
||||||
|
SearchCriteria criteria = new SearchCriteria(); |
||||||
|
criteria.setOffset(10); |
||||||
|
criteria.setCount(Integer.MAX_VALUE); |
||||||
|
criteria.setQuery(QUERY_ENG_ONLY); |
||||||
|
|
||||||
|
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST_ID3); |
||||||
|
assertEquals("SearchSong", "+((artist:abc*) (artist:def*)) +spanOr([folderId:" |
||||||
|
+ NumericUtils.intToPrefixCoded(FID1) + "])", query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ARTIST_ID3); |
||||||
|
assertEquals("SearchSong", |
||||||
|
"+((artist:abc*) (artist:def*)) +spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) |
||||||
|
+ ", folderId:" + NumericUtils.intToPrefixCoded(FID2) + "])", |
||||||
|
query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchAlbumId3() throws ParseException, IOException { |
||||||
|
SearchCriteria criteria = new SearchCriteria(); |
||||||
|
criteria.setOffset(10); |
||||||
|
criteria.setCount(Integer.MAX_VALUE); |
||||||
|
criteria.setQuery(QUERY_ENG_ONLY); |
||||||
|
|
||||||
|
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ALBUM_ID3); |
||||||
|
assertEquals( |
||||||
|
"SearchAlbumId3", "+((album:abc* artist:abc* folderId:abc*) (album:def* artist:def* folderId:def*)) " |
||||||
|
+ "+spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.search(criteria, MULTI_FOLDERS, IndexType.ALBUM_ID3); |
||||||
|
assertEquals("SearchAlbumId3", |
||||||
|
"+((album:abc* artist:abc* folderId:abc*) (album:def* artist:def* folderId:def*)) +spanOr([folderId:" |
||||||
|
+ NumericUtils.intToPrefixCoded(FID1) + ", folderId:" |
||||||
|
+ NumericUtils.intToPrefixCoded(FID2) + "])", |
||||||
|
query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchByNameArtist() throws ParseException { |
||||||
|
Query query = queryFactory.searchByName(FieldNames.ARTIST, QUERY_ENG_ONLY); |
||||||
|
assertEquals("SearchByNameArtist", "artist:abc artist:def*", query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchByNameAlbum() throws ParseException { |
||||||
|
Query query = queryFactory.searchByName(FieldNames.ALBUM, QUERY_ENG_ONLY); |
||||||
|
assertEquals("SearchByNameAlbum", "album:abc album:def*", query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchByNameTitle() throws ParseException { |
||||||
|
Query query = queryFactory.searchByName(FieldNames.TITLE, QUERY_ENG_ONLY); |
||||||
|
assertEquals("SearchByNameTitle", "title:abc title:def*", query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetRandomSongs() { |
||||||
|
RandomSearchCriteria criteria = new RandomSearchCriteria(50, "Classic Rock", |
||||||
|
Integer.valueOf(1900), Integer.valueOf(2000), SINGLE_FOLDERS); |
||||||
|
|
||||||
|
Query query = queryFactory.getRandomSongs(criteria); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(criteria), |
||||||
|
"+mediaType:music +genre:classicrock +year:[1900 TO 2000] +spanOr([folder:" + PATH1 + "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
criteria = new RandomSearchCriteria(50, "Classic Rock", Integer.valueOf(1900), |
||||||
|
Integer.valueOf(2000), MULTI_FOLDERS); |
||||||
|
query = queryFactory.getRandomSongs(criteria); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(criteria), |
||||||
|
"+mediaType:music +genre:classicrock +year:[1900 TO 2000] +spanOr([folder:" + PATH1 + ", folder:" + PATH2 |
||||||
|
+ "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
criteria = new RandomSearchCriteria(50, "Classic Rock", null, null, MULTI_FOLDERS); |
||||||
|
query = queryFactory.getRandomSongs(criteria); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(criteria), |
||||||
|
"+mediaType:music +genre:classicrock +spanOr([folder:" + PATH1 + ", folder:" + PATH2 + "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
criteria = new RandomSearchCriteria(50, "Classic Rock", Integer.valueOf(1900), null, |
||||||
|
MULTI_FOLDERS); |
||||||
|
query = queryFactory.getRandomSongs(criteria); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(criteria), |
||||||
|
"+mediaType:music +genre:classicrock +year:[1900 TO *] +spanOr([folder:" + PATH1 + ", folder:" + PATH2 |
||||||
|
+ "])", |
||||||
|
query.toString()); |
||||||
|
|
||||||
|
criteria = new RandomSearchCriteria(50, "Classic Rock", null, Integer.valueOf(2000), |
||||||
|
MULTI_FOLDERS); |
||||||
|
query = queryFactory.getRandomSongs(criteria); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(criteria), |
||||||
|
"+mediaType:music +genre:classicrock +year:[* TO 2000] +spanOr([folder:" + PATH1 + ", folder:" + PATH2 |
||||||
|
+ "])", |
||||||
|
query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetRandomAlbums() { |
||||||
|
Query query = queryFactory.getRandomAlbums(SINGLE_FOLDERS); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(SINGLE_FOLDERS), |
||||||
|
"spanOr([folder:" + PATH1 + "])", query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.getRandomAlbums(MULTI_FOLDERS); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(MULTI_FOLDERS), |
||||||
|
"spanOr([folder:" + PATH1 + ", folder:" + PATH2 + "])", query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetRandomAlbumsId3() { |
||||||
|
Query query = queryFactory.getRandomAlbumsId3(SINGLE_FOLDERS); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(SINGLE_FOLDERS), |
||||||
|
"spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + "])", query.toString()); |
||||||
|
|
||||||
|
query = queryFactory.getRandomAlbumsId3(MULTI_FOLDERS); |
||||||
|
assertEquals(ToStringBuilder.reflectionToString(MULTI_FOLDERS), |
||||||
|
"spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + ", folderId:" |
||||||
|
+ NumericUtils.intToPrefixCoded(FID2) + "])", |
||||||
|
query.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,466 @@ |
|||||||
|
|
||||||
|
package org.airsonic.player.service.search; |
||||||
|
|
||||||
|
import com.codahale.metrics.ConsoleReporter; |
||||||
|
import com.codahale.metrics.MetricRegistry; |
||||||
|
import com.codahale.metrics.Timer; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Random; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
import org.airsonic.player.TestCaseUtils; |
||||||
|
import org.airsonic.player.dao.AlbumDao; |
||||||
|
import org.airsonic.player.dao.DaoHelper; |
||||||
|
import org.airsonic.player.dao.MediaFileDao; |
||||||
|
import org.airsonic.player.dao.MusicFolderDao; |
||||||
|
import org.airsonic.player.dao.MusicFolderTestData; |
||||||
|
import org.airsonic.player.domain.Album; |
||||||
|
import org.airsonic.player.domain.Artist; |
||||||
|
import org.airsonic.player.domain.MediaFile; |
||||||
|
import org.airsonic.player.domain.MediaFile.MediaType; |
||||||
|
import org.airsonic.player.domain.MusicFolder; |
||||||
|
import org.airsonic.player.domain.ParamSearchResult; |
||||||
|
import org.airsonic.player.domain.RandomSearchCriteria; |
||||||
|
import org.airsonic.player.domain.SearchCriteria; |
||||||
|
import org.airsonic.player.domain.SearchResult; |
||||||
|
import org.airsonic.player.service.MediaScannerService; |
||||||
|
import org.airsonic.player.service.SearchService; |
||||||
|
import org.airsonic.player.service.SettingsService; |
||||||
|
import org.airsonic.player.service.search.IndexType; |
||||||
|
import org.airsonic.player.util.HomeRule; |
||||||
|
import org.junit.Assert; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.ClassRule; |
||||||
|
import org.junit.Rule; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.rules.TemporaryFolder; |
||||||
|
import org.junit.runner.Description; |
||||||
|
import org.junit.runners.model.Statement; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.core.io.ResourceLoader; |
||||||
|
import org.springframework.test.annotation.DirtiesContext; |
||||||
|
import org.springframework.test.context.ContextConfiguration; |
||||||
|
import org.springframework.test.context.junit4.rules.SpringClassRule; |
||||||
|
import org.springframework.test.context.junit4.rules.SpringMethodRule; |
||||||
|
import org.subsonic.restapi.ArtistID3; |
||||||
|
|
||||||
|
@ContextConfiguration( |
||||||
|
locations = { |
||||||
|
"/applicationContext-service.xml", |
||||||
|
"/applicationContext-cache.xml", |
||||||
|
"/applicationContext-testdb.xml", |
||||||
|
"/applicationContext-mockSonos.xml" }) |
||||||
|
@DirtiesContext( |
||||||
|
classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) |
||||||
|
public class SearchServiceTestCase { |
||||||
|
|
||||||
|
@ClassRule |
||||||
|
public static final SpringClassRule classRule = new SpringClassRule() { |
||||||
|
HomeRule homeRule = new HomeRule(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public Statement apply(Statement base, Description description) { |
||||||
|
Statement spring = super.apply(base, description); |
||||||
|
return homeRule.apply(spring, description); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
@Rule |
||||||
|
public final SpringMethodRule springMethodRule = new SpringMethodRule(); |
||||||
|
|
||||||
|
private final MetricRegistry metrics = new MetricRegistry(); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MediaScannerService mediaScannerService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MediaFileDao mediaFileDao; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private MusicFolderDao musicFolderDao; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private DaoHelper daoHelper; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private AlbumDao albumDao; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private SearchService searchService; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private SettingsService settingsService; |
||||||
|
|
||||||
|
@Rule |
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder(); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
ResourceLoader resourceLoader; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setup() throws Exception { |
||||||
|
populateDatabase(); |
||||||
|
} |
||||||
|
|
||||||
|
private static boolean dataBasePopulated; |
||||||
|
|
||||||
|
private int count = 1; |
||||||
|
|
||||||
|
/* |
||||||
|
* Cases susceptible to SerchService refactoring and version upgrades. |
||||||
|
* It is not exhaustive. |
||||||
|
*/ |
||||||
|
private synchronized void populateDatabase() { |
||||||
|
|
||||||
|
/* |
||||||
|
* It seems that there is a case that does not work well |
||||||
|
* if you test immediately after initialization in 1 method. |
||||||
|
* It may be improved later. |
||||||
|
*/ |
||||||
|
try { |
||||||
|
Thread.sleep(300 * count++); |
||||||
|
} catch (InterruptedException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
if (!dataBasePopulated) { |
||||||
|
|
||||||
|
MusicFolderTestData.getTestMusicFolders().forEach(musicFolderDao::createMusicFolder); |
||||||
|
settingsService.clearMusicFolderCache(); |
||||||
|
TestCaseUtils.execScan(mediaScannerService); |
||||||
|
System.out.println("--- Report of records count per table ---"); |
||||||
|
Map<String, Integer> records = TestCaseUtils.recordsInAllTables(daoHelper); |
||||||
|
records.keySet().stream().filter(s -> s.equals("MEDIA_FILE") // 20
|
||||||
|
| s.equals("ARTIST") // 5
|
||||||
|
| s.equals("MUSIC_FOLDER")// 3
|
||||||
|
| s.equals("ALBUM"))// 5
|
||||||
|
.forEach(tableName -> System.out |
||||||
|
.println("\t" + tableName + " : " + records.get(tableName).toString())); |
||||||
|
// Music Folder Music must have 3 children
|
||||||
|
List<MediaFile> listeMusicChildren = mediaFileDao.getChildrenOf( |
||||||
|
new File(MusicFolderTestData.resolveMusicFolderPath()).getPath()); |
||||||
|
Assert.assertEquals(3, listeMusicChildren.size()); |
||||||
|
// Music Folder Music2 must have 1 children
|
||||||
|
List<MediaFile> listeMusic2Children = mediaFileDao.getChildrenOf( |
||||||
|
new File(MusicFolderTestData.resolveMusic2FolderPath()).getPath()); |
||||||
|
Assert.assertEquals(1, listeMusic2Children.size()); |
||||||
|
System.out.println("--- *********************** ---"); |
||||||
|
dataBasePopulated = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSearchTypical() { |
||||||
|
|
||||||
|
/* |
||||||
|
* A simple test that is expected to easily detect API syntax differences when updating lucene. |
||||||
|
* Complete route coverage and data coverage in this case alone are not conscious. |
||||||
|
*/ |
||||||
|
|
||||||
|
List<MusicFolder> allMusicFolders = musicFolderDao.getAllMusicFolders(); |
||||||
|
Assert.assertEquals(3, allMusicFolders.size()); |
||||||
|
|
||||||
|
// *** testSearch() ***
|
||||||
|
|
||||||
|
String query = "Sarah Walker"; |
||||||
|
final SearchCriteria searchCriteria = new SearchCriteria(); |
||||||
|
searchCriteria.setQuery(query); |
||||||
|
searchCriteria.setCount(Integer.MAX_VALUE); |
||||||
|
searchCriteria.setOffset(0); |
||||||
|
|
||||||
|
/* |
||||||
|
* _ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble |
||||||
|
* Should find any version of Lucene. |
||||||
|
*/ |
||||||
|
SearchResult result = searchService.search(searchCriteria, allMusicFolders, |
||||||
|
IndexType.ALBUM); |
||||||
|
Assert.assertEquals("(0) Specify '" + query + "' as query, total Hits is", 1, |
||||||
|
result.getTotalHits()); |
||||||
|
Assert.assertEquals("(1) Specify artist '" + query + "' as query. Artist SIZE is", 0, |
||||||
|
result.getArtists().size()); |
||||||
|
Assert.assertEquals("(2) Specify artist '" + query + "' as query. Album SIZE is", 0, |
||||||
|
result.getAlbums().size()); |
||||||
|
Assert.assertEquals("(3) Specify artist '" + query + "' as query, MediaFile SIZE is", 1, |
||||||
|
result.getMediaFiles().size()); |
||||||
|
Assert.assertEquals("(4) ", MediaType.ALBUM, result.getMediaFiles().get(0).getMediaType()); |
||||||
|
Assert.assertEquals( |
||||||
|
"(5) Specify artist '" + query + "' as query, and get a album. Name is ", |
||||||
|
"_ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble", |
||||||
|
result.getMediaFiles().get(0).getArtist()); |
||||||
|
Assert.assertEquals( |
||||||
|
"(6) Specify artist '" + query + "' as query, and get a album. Name is ", |
||||||
|
"_ID3_ALBUM_ Ravel - Chamber Music With Voice", |
||||||
|
result.getMediaFiles().get(0).getAlbumName()); |
||||||
|
|
||||||
|
/* |
||||||
|
* _ID3_ALBUM_ Ravel - Chamber Music With Voice |
||||||
|
* Should find any version of Lucene. |
||||||
|
*/ |
||||||
|
query = "music"; |
||||||
|
searchCriteria.setQuery(query); |
||||||
|
result = searchService.search(searchCriteria, allMusicFolders, IndexType.ALBUM_ID3); |
||||||
|
Assert.assertEquals("Specify '" + query + "' as query, total Hits is", 1, |
||||||
|
result.getTotalHits()); |
||||||
|
Assert.assertEquals("(7) Specify '" + query + "' as query, and get a song. Artist SIZE is ", |
||||||
|
0, result.getArtists().size()); |
||||||
|
Assert.assertEquals("(8) Specify '" + query + "' as query, and get a song. Album SIZE is ", |
||||||
|
1, result.getAlbums().size()); |
||||||
|
Assert.assertEquals( |
||||||
|
"(9) Specify '" + query + "' as query, and get a song. MediaFile SIZE is ", 0, |
||||||
|
result.getMediaFiles().size()); |
||||||
|
Assert.assertEquals("(9) Specify '" + query + "' as query, and get a album. Name is ", |
||||||
|
"_ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble", |
||||||
|
result.getAlbums().get(0).getArtist()); |
||||||
|
Assert.assertEquals("(10) Specify '" + query + "' as query, and get a album. Name is ", |
||||||
|
"_ID3_ALBUM_ Ravel - Chamber Music With Voice", |
||||||
|
result.getAlbums().get(0).getName()); |
||||||
|
|
||||||
|
/* |
||||||
|
* _ID3_ALBUM_ Ravel - Chamber Music With Voice |
||||||
|
* Should find any version of Lucene. |
||||||
|
*/ |
||||||
|
query = "Ravel - Chamber Music"; |
||||||
|
searchCriteria.setQuery(query); |
||||||
|
result = searchService.search(searchCriteria, allMusicFolders, IndexType.SONG); |
||||||
|
Assert.assertEquals("(11) Specify album '" + query + "' as query, total Hits is", 2, |
||||||
|
result.getTotalHits()); |
||||||
|
Assert.assertEquals("(12) Specify album '" + query + "', and get a song. Artist SIZE is", 0, |
||||||
|
result.getArtists().size()); |
||||||
|
Assert.assertEquals("(13) Specify album '" + query + "', and get a song. Album SIZE is", 0, |
||||||
|
result.getAlbums().size()); |
||||||
|
Assert.assertEquals("(14) Specify album '" + query + "', and get a song. MediaFile SIZE is", |
||||||
|
2, result.getMediaFiles().size()); |
||||||
|
Assert.assertEquals("(15) Specify album '" + query + "', and get songs. The first song is ", |
||||||
|
"01 - Gaspard de la Nuit - i. Ondine", result.getMediaFiles().get(0).getTitle()); |
||||||
|
Assert.assertEquals( |
||||||
|
"(16) Specify album '" + query + "', and get songs. The second song is ", |
||||||
|
"02 - Gaspard de la Nuit - ii. Le Gibet", result.getMediaFiles().get(1).getTitle()); |
||||||
|
|
||||||
|
// *** testSearchByName() ***
|
||||||
|
|
||||||
|
/* |
||||||
|
* _ID3_ALBUM_ Sackcloth 'n' Ashes |
||||||
|
* Should be 1 in Lucene 3.0(Because Single quate is not a delimiter). |
||||||
|
*/ |
||||||
|
query = "Sackcloth 'n' Ashes"; |
||||||
|
ParamSearchResult<Album> albumResult = searchService.searchByName(query, 0, |
||||||
|
Integer.MAX_VALUE, allMusicFolders, Album.class); |
||||||
|
Assert.assertEquals( |
||||||
|
"(17) Specify album name '" + query + "' as the name, and get an album.", 1, |
||||||
|
albumResult.getItems().size()); |
||||||
|
Assert.assertEquals("(18) Specify '" + query + "' as the name, The album name is ", |
||||||
|
"_ID3_ALBUM_ Sackcloth 'n' Ashes", albumResult.getItems().get(0).getName()); |
||||||
|
Assert.assertEquals( |
||||||
|
"(19) Whether the acquired album contains data of the specified album name", 1L, |
||||||
|
albumResult.getItems().stream() |
||||||
|
.filter(r -> "_ID3_ALBUM_ Sackcloth \'n\' Ashes".equals(r.getName())) |
||||||
|
.count()); |
||||||
|
|
||||||
|
/* |
||||||
|
* Should be 0 in Lucene 3.0(Since the slash is not a delimiter). |
||||||
|
*/ |
||||||
|
query = "lker/Nash"; |
||||||
|
ParamSearchResult<ArtistID3> artistId3Result = searchService.searchByName(query, 0, |
||||||
|
Integer.MAX_VALUE, allMusicFolders, ArtistID3.class); |
||||||
|
Assert.assertEquals("(20) Specify '" + query + "' as the name, and get an artist.", 0, |
||||||
|
artistId3Result.getItems().size()); |
||||||
|
ParamSearchResult<Artist> artistResult = searchService.searchByName(query, 0, |
||||||
|
Integer.MAX_VALUE, allMusicFolders, Artist.class); |
||||||
|
Assert.assertEquals("(21) Specify '" + query + "' as the name, and get an artist.", 0, |
||||||
|
artistResult.getItems().size()); |
||||||
|
|
||||||
|
// *** testGetRandomSongs() ***
|
||||||
|
|
||||||
|
/* |
||||||
|
* Regardless of the Lucene version, |
||||||
|
* RandomSearchCriteria can specify null and means the maximum range. |
||||||
|
* 11 should be obtainable. |
||||||
|
*/ |
||||||
|
RandomSearchCriteria randomSearchCriteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||||
|
null, // genre,
|
||||||
|
null, // fromYear
|
||||||
|
null, // toYear
|
||||||
|
allMusicFolders // musicFolders
|
||||||
|
); |
||||||
|
List<MediaFile> allRandomSongs = searchService.getRandomSongs(randomSearchCriteria); |
||||||
|
Assert.assertEquals( |
||||||
|
"(22) Specify MAX_VALUE as the upper limit, and randomly acquire songs.", 11, |
||||||
|
allRandomSongs.size()); |
||||||
|
|
||||||
|
/* |
||||||
|
* Regardless of the Lucene version, |
||||||
|
* 7 should be obtainable. |
||||||
|
*/ |
||||||
|
randomSearchCriteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||||
|
null, // genre,
|
||||||
|
1900, // fromYear
|
||||||
|
null, // toYear
|
||||||
|
allMusicFolders // musicFolders
|
||||||
|
); |
||||||
|
allRandomSongs = searchService.getRandomSongs(randomSearchCriteria); |
||||||
|
Assert.assertEquals("(23) Specify 1900 as 'fromYear', and randomly acquire songs.", 7, |
||||||
|
allRandomSongs.size()); |
||||||
|
|
||||||
|
/* |
||||||
|
* Regardless of the Lucene version, |
||||||
|
* It should be 0 because it is a non-existent genre. |
||||||
|
*/ |
||||||
|
randomSearchCriteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||||
|
"Chamber Music", // genre,
|
||||||
|
null, // fromYear
|
||||||
|
null, // toYear
|
||||||
|
allMusicFolders // musicFolders
|
||||||
|
); |
||||||
|
allRandomSongs = searchService.getRandomSongs(randomSearchCriteria); |
||||||
|
Assert.assertEquals("(24) Specify music as 'genre', and randomly acquire songs.", 0, |
||||||
|
allRandomSongs.size()); |
||||||
|
|
||||||
|
// *** testGetRandomAlbums() ***
|
||||||
|
|
||||||
|
/* |
||||||
|
* Acquisition of maximum number(5). |
||||||
|
*/ |
||||||
|
List<Album> allAlbums = albumDao.getAlphabeticalAlbums(0, 0, true, true, allMusicFolders); |
||||||
|
Assert.assertEquals("(25) Get all albums with Dao.", 5, allAlbums.size()); |
||||||
|
List<MediaFile> allRandomAlbums = searchService.getRandomAlbums(Integer.MAX_VALUE, |
||||||
|
allMusicFolders); |
||||||
|
Assert.assertEquals("(26) Specify Integer.MAX_VALUE as the upper limit," |
||||||
|
+ "and randomly acquire albums(file struct).", 5, allRandomAlbums.size()); |
||||||
|
|
||||||
|
/* |
||||||
|
* Acquisition of maximum number(5). |
||||||
|
*/ |
||||||
|
List<Album> allRandomAlbumsId3 = searchService.getRandomAlbumsId3(Integer.MAX_VALUE, |
||||||
|
allMusicFolders); |
||||||
|
Assert.assertEquals( |
||||||
|
"(27) Specify Integer.MAX_VALUE as the upper limit, and randomly acquire albums(ID3).", |
||||||
|
5, allRandomAlbumsId3.size()); |
||||||
|
|
||||||
|
/* |
||||||
|
* Total is 4. |
||||||
|
*/ |
||||||
|
query = "ID 3 ARTIST"; |
||||||
|
searchCriteria.setQuery(query); |
||||||
|
result = searchService.search(searchCriteria, allMusicFolders, IndexType.ARTIST_ID3); |
||||||
|
Assert.assertEquals("(28) Specify '" + query + "', total Hits is", 4, |
||||||
|
result.getTotalHits()); |
||||||
|
Assert.assertEquals("(29) Specify '" + query + "', and get an artists. Artist SIZE is ", 4, |
||||||
|
result.getArtists().size()); |
||||||
|
Assert.assertEquals("(30) Specify '" + query + "', and get a artists. Album SIZE is ", 0, |
||||||
|
result.getAlbums().size()); |
||||||
|
Assert.assertEquals("(31) Specify '" + query + "', and get a artists. MediaFile SIZE is ", |
||||||
|
0, result.getMediaFiles().size()); |
||||||
|
|
||||||
|
/* |
||||||
|
* Three hits to the artist. |
||||||
|
* ALBUMARTIST is not registered with these. |
||||||
|
* Therefore, the registered value of ARTIST is substituted in ALBUMARTIST. |
||||||
|
*/ |
||||||
|
long count = result.getArtists().stream() |
||||||
|
.filter(a -> a.getName().startsWith("_ID3_ARTIST_")).count(); |
||||||
|
Assert.assertEquals("(32) Artist whose name contains \\\"_ID3_ARTIST_\\\" is 3 records.", |
||||||
|
3L, count); |
||||||
|
|
||||||
|
/* |
||||||
|
* The structure of "01 - Sonata Violin & Cello I. Allegro.ogg" |
||||||
|
* ARTIST -> _ID3_ARTIST_ Sarah Walker/Nash Ensemble |
||||||
|
* ALBUMARTIST -> _ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble |
||||||
|
* (The result must not contain duplicates. And ALBUMARTIST must be returned correctly.) |
||||||
|
*/ |
||||||
|
count = result.getArtists().stream() |
||||||
|
.filter(a -> a.getName().startsWith("_ID3_ALBUMARTIST_")).count(); |
||||||
|
Assert.assertEquals("(33) Artist whose name is \"_ID3_ARTIST_\" is 1 records.", 1L, count); |
||||||
|
|
||||||
|
/* |
||||||
|
* Below is a simple loop test. |
||||||
|
* How long is the total time? |
||||||
|
*/ |
||||||
|
int countForEachMethod = 500; |
||||||
|
String[] randomWords4Search = createRandomWords(countForEachMethod); |
||||||
|
String[] randomWords4SearchByName = createRandomWords(countForEachMethod); |
||||||
|
|
||||||
|
Timer globalTimer = metrics |
||||||
|
.timer(MetricRegistry.name(SearchServiceTestCase.class, "Timer.global")); |
||||||
|
final Timer.Context globalTimerContext = globalTimer.time(); |
||||||
|
|
||||||
|
System.out.println("--- Random search (" + countForEachMethod * 5 + " times) ---"); |
||||||
|
|
||||||
|
// testSearch()
|
||||||
|
Arrays.stream(randomWords4Search).forEach(w -> { |
||||||
|
searchCriteria.setQuery(w); |
||||||
|
searchService.search(searchCriteria, allMusicFolders, IndexType.ALBUM); |
||||||
|
}); |
||||||
|
|
||||||
|
// testSearchByName()
|
||||||
|
Arrays.stream(randomWords4SearchByName).forEach(w -> { |
||||||
|
searchService.searchByName(w, 0, Integer.MAX_VALUE, allMusicFolders, Artist.class); |
||||||
|
}); |
||||||
|
|
||||||
|
// testGetRandomSongs()
|
||||||
|
RandomSearchCriteria criteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||||
|
null, // genre,
|
||||||
|
null, // fromYear
|
||||||
|
null, // toYear
|
||||||
|
allMusicFolders // musicFolders
|
||||||
|
); |
||||||
|
for (int i = 0; i < countForEachMethod; i++) { |
||||||
|
searchService.getRandomSongs(criteria); |
||||||
|
} |
||||||
|
|
||||||
|
// testGetRandomAlbums()
|
||||||
|
for (int i = 0; i < countForEachMethod; i++) { |
||||||
|
searchService.getRandomAlbums(Integer.MAX_VALUE, allMusicFolders); |
||||||
|
} |
||||||
|
|
||||||
|
// testGetRandomAlbumsId3()
|
||||||
|
for (int i = 0; i < countForEachMethod; i++) { |
||||||
|
searchService.getRandomAlbumsId3(Integer.MAX_VALUE, allMusicFolders); |
||||||
|
} |
||||||
|
|
||||||
|
globalTimerContext.stop(); |
||||||
|
|
||||||
|
/* |
||||||
|
* Whether or not IndexReader is exhausted. |
||||||
|
*/ |
||||||
|
query = "Sarah Walker"; |
||||||
|
searchCriteria.setQuery(query); |
||||||
|
result = searchService.search(searchCriteria, allMusicFolders, IndexType.ALBUM); |
||||||
|
Assert.assertEquals("(35) Can the normal case be implemented.", 0, |
||||||
|
result.getArtists().size()); |
||||||
|
Assert.assertEquals("(36) Can the normal case be implemented.", 0, |
||||||
|
result.getAlbums().size()); |
||||||
|
Assert.assertEquals("(37) Can the normal case be implemented.", 1, |
||||||
|
result.getMediaFiles().size()); |
||||||
|
Assert.assertEquals("(38) Can the normal case be implemented.", MediaType.ALBUM, |
||||||
|
result.getMediaFiles().get(0).getMediaType()); |
||||||
|
Assert.assertEquals("(39) Can the normal case be implemented.", |
||||||
|
"_ID3_ALBUMARTIST_ Sarah Walker/Nash Ensemble", |
||||||
|
result.getMediaFiles().get(0).getArtist()); |
||||||
|
|
||||||
|
System.out.println("--- SUCCESS ---"); |
||||||
|
|
||||||
|
ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics) |
||||||
|
.convertRatesTo(TimeUnit.SECONDS).convertDurationsTo(TimeUnit.MILLISECONDS).build(); |
||||||
|
reporter.report(); |
||||||
|
|
||||||
|
System.out.println("End. "); |
||||||
|
} |
||||||
|
|
||||||
|
private static String[] createRandomWords(int count) { |
||||||
|
String[] randomStrings = new String[count]; |
||||||
|
Random random = new Random(); |
||||||
|
for (int i = 0; i < count; i++) { |
||||||
|
char[] word = new char[random.nextInt(8) + 3]; |
||||||
|
for (int j = 0; j < word.length; j++) { |
||||||
|
word[j] = (char) ('a' + random.nextInt(26)); |
||||||
|
} |
||||||
|
randomStrings[i] = new String(word); |
||||||
|
} |
||||||
|
return randomStrings; |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue