Update Lucene from 3.0.3 to the current version 8.2.0
Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import org.airsonic.player.dao.AlbumDao;
|
||||
import org.airsonic.player.dao.ArtistDao;
|
||||
import org.airsonic.player.dao.MediaFileDao;
|
||||
import org.airsonic.player.domain.*;
|
||||
import org.airsonic.player.util.FileUtil;
|
||||
import org.airsonic.player.service.search.IndexManager;
|
||||
import org.apache.commons.lang.ObjectUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.slf4j.Logger;
|
||||
@@ -44,7 +44,6 @@ import java.util.*;
|
||||
@Service
|
||||
public class MediaScannerService {
|
||||
|
||||
private static final int INDEX_VERSION = 15;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MediaScannerService.class);
|
||||
|
||||
private MediaLibraryStatistics statistics;
|
||||
@@ -54,7 +53,7 @@ public class MediaScannerService {
|
||||
@Autowired
|
||||
private SettingsService settingsService;
|
||||
@Autowired
|
||||
private SearchService searchService;
|
||||
private IndexManager indexManager;
|
||||
@Autowired
|
||||
private PlaylistService playlistService;
|
||||
@Autowired
|
||||
@@ -69,13 +68,13 @@ public class MediaScannerService {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
deleteOldIndexFiles();
|
||||
indexManager.initializeIndexDirectory();
|
||||
statistics = settingsService.getMediaLibraryStatistics();
|
||||
schedule();
|
||||
}
|
||||
|
||||
public void initNoSchedule() {
|
||||
deleteOldIndexFiles();
|
||||
indexManager.deleteOldIndexFiles();
|
||||
statistics = settingsService.getMediaLibraryStatistics();
|
||||
}
|
||||
|
||||
@@ -179,7 +178,7 @@ public class MediaScannerService {
|
||||
statistics.reset();
|
||||
|
||||
mediaFileService.setMemoryCacheEnabled(false);
|
||||
searchService.startIndexing();
|
||||
indexManager.startIndexing();
|
||||
|
||||
mediaFileService.clearMemoryCache();
|
||||
|
||||
@@ -223,7 +222,7 @@ public class MediaScannerService {
|
||||
LOG.error("Failed to scan media library.", x);
|
||||
} finally {
|
||||
mediaFileService.setMemoryCacheEnabled(true);
|
||||
searchService.stopIndexing();
|
||||
indexManager.stopIndexing();
|
||||
scanning = false;
|
||||
}
|
||||
}
|
||||
@@ -243,7 +242,7 @@ public class MediaScannerService {
|
||||
mediaFileDao.createOrUpdateMediaFile(file);
|
||||
}
|
||||
|
||||
searchService.index(file);
|
||||
indexManager.index(file);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) {
|
||||
@@ -331,7 +330,7 @@ public class MediaScannerService {
|
||||
album.setPresent(true);
|
||||
albumDao.createOrUpdateAlbum(album);
|
||||
if (firstEncounter) {
|
||||
searchService.index(album);
|
||||
indexManager.index(album);
|
||||
}
|
||||
|
||||
// Update the file's album artist, if necessary.
|
||||
@@ -370,7 +369,7 @@ public class MediaScannerService {
|
||||
artistDao.createOrUpdateArtist(artist);
|
||||
|
||||
if (firstEncounter) {
|
||||
searchService.index(artist, musicFolder);
|
||||
indexManager.index(artist, musicFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,43 +382,10 @@ public class MediaScannerService {
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes old versions of the index file.
|
||||
*/
|
||||
private void deleteOldIndexFiles() {
|
||||
for (int i = 2; i < INDEX_VERSION; i++) {
|
||||
File file = getIndexFile(i);
|
||||
try {
|
||||
if (FileUtil.exists(file)) {
|
||||
if (file.delete()) {
|
||||
LOG.info("Deleted old index file: " + file.getPath());
|
||||
}
|
||||
}
|
||||
} catch (Exception x) {
|
||||
LOG.warn("Failed to delete old index file: " + file.getPath(), x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index file for the given index version.
|
||||
*
|
||||
* @param version The index version.
|
||||
* @return The index file for the given index version.
|
||||
*/
|
||||
private File getIndexFile(int version) {
|
||||
File home = SettingsService.getAirsonicHome();
|
||||
return new File(home, "airsonic" + version + ".index");
|
||||
}
|
||||
|
||||
public void setSettingsService(SettingsService settingsService) {
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void setSearchService(SearchService searchService) {
|
||||
this.searchService = searchService;
|
||||
}
|
||||
|
||||
public void setMediaFileService(MediaFileService mediaFileService) {
|
||||
this.mediaFileService = mediaFileService;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package org.airsonic.player.service;
|
||||
|
||||
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.domain.ParamSearchResult;
|
||||
@@ -33,7 +32,7 @@ import org.airsonic.player.service.search.IndexType;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Performs Lucene-based searching and indexing.
|
||||
* Performs Lucene-based searching.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
@@ -41,16 +40,6 @@ import java.util.List;
|
||||
*/
|
||||
public interface SearchService {
|
||||
|
||||
void startIndexing();
|
||||
|
||||
void index(MediaFile mediaFile);
|
||||
|
||||
void index(Artist artist, MusicFolder musicFolder);
|
||||
|
||||
void index(Album album);
|
||||
|
||||
void stopIndexing();
|
||||
|
||||
SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders,
|
||||
IndexType indexType);
|
||||
|
||||
|
||||
+178
-121
@@ -1,121 +1,178 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.Analyzer;
|
||||
import org.apache.lucene.analysis.cjk.CJKWidthFilterFactory;
|
||||
import org.apache.lucene.analysis.core.KeywordTokenizerFactory;
|
||||
import org.apache.lucene.analysis.core.LowerCaseFilterFactory;
|
||||
import org.apache.lucene.analysis.core.StopFilterFactory;
|
||||
import org.apache.lucene.analysis.custom.CustomAnalyzer;
|
||||
import org.apache.lucene.analysis.custom.CustomAnalyzer.Builder;
|
||||
import org.apache.lucene.analysis.en.EnglishPossessiveFilterFactory;
|
||||
import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilterFactory;
|
||||
import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
|
||||
import org.apache.lucene.analysis.pattern.PatternReplaceFilterFactory;
|
||||
import org.apache.lucene.analysis.standard.StandardTokenizerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x : Convert UAX#29 Underscore Analysis to Legacy Analysis
|
||||
*
|
||||
* Because changes in underscores before and after words
|
||||
* have a major effect on user's forward match search.
|
||||
*
|
||||
* @see AnalyzerFactoryTestCase
|
||||
*/
|
||||
private void addTokenFilterForUnderscoreRemovalAroundToken(Builder builder) throws IOException {
|
||||
builder
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "^\\_", "replacement", "", "replace", "all")
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "\\_$", "replacement", "", "replace", "all");
|
||||
}
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x : Handle brackets correctly
|
||||
*
|
||||
* Process the input value of Genre search for search of domain value.
|
||||
*
|
||||
* The tag parser performs special character conversion
|
||||
* when converting input values from a file.
|
||||
* Therefore, the domain value may be different from the original value.
|
||||
* This filter allows searching by user readable value (file tag value).
|
||||
*
|
||||
* @see org.jaudiotagger.tag.id3.framebody.FrameBodyTCON#convertID3v23GenreToGeneric
|
||||
* (TCON stands for Genre with ID3 v2.3-v2.4)
|
||||
* Such processing exists because brackets in the Gener string have a special meaning.
|
||||
*/
|
||||
private void addTokenFilterForTokenToDomainValue(Builder builder) throws IOException {
|
||||
builder
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "\\(", "replacement", "", "replace", "all")
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "\\)$", "replacement", "", "replace", "all")
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "\\)", "replacement", " ", "replace", "all")
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "\\{\\}", "replacement", "\\{ \\}", "replace", "all")
|
||||
.addTokenFilter(PatternReplaceFilterFactory.class,
|
||||
"pattern", "\\[\\]", "replacement", "\\[ \\]", "replace", "all");
|
||||
}
|
||||
|
||||
private Builder createDefaultAnalyzerBuilder() throws IOException {
|
||||
Builder builder = CustomAnalyzer.builder()
|
||||
.withTokenizer(StandardTokenizerFactory.class)
|
||||
.addTokenFilter(CJKWidthFilterFactory.class)
|
||||
.addTokenFilter(ASCIIFoldingFilterFactory.class, "preserveOriginal", "false")
|
||||
.addTokenFilter(LowerCaseFilterFactory.class)
|
||||
.addTokenFilter(StopFilterFactory.class)
|
||||
.addTokenFilter(EnglishPossessiveFilterFactory.class);
|
||||
addTokenFilterForUnderscoreRemovalAroundToken(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private Builder createKeywordAnalyzerBuilder() throws IOException {
|
||||
return CustomAnalyzer.builder()
|
||||
.withTokenizer(KeywordTokenizerFactory.class);
|
||||
}
|
||||
|
||||
private Builder createGenreAnalyzerBuilder() throws IOException {
|
||||
Builder builder = createKeywordAnalyzerBuilder();
|
||||
addTokenFilterForTokenToDomainValue(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Analyzer to use when generating the index.
|
||||
*
|
||||
* Whether this analyzer is applied to input values depends on
|
||||
* the definition of the document's fields.
|
||||
*
|
||||
* @return analyzer for index
|
||||
* @see DocumentFactory
|
||||
*/
|
||||
public Analyzer getAnalyzer() throws IOException {
|
||||
if (isEmpty(analyzer)) {
|
||||
try {
|
||||
analyzer = createDefaultAnalyzerBuilder().build();
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Error when initializing Analyzer.", e);
|
||||
}
|
||||
}
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the analyzer to use when generating a query for index search.
|
||||
*
|
||||
* String processing handled by QueryFactory
|
||||
* is limited to Lucene's modifier.
|
||||
*
|
||||
* The processing of the operands is expressed
|
||||
* in the AnalyzerFactory implementation.
|
||||
* Rules for tokenizing/converting input values
|
||||
* should not be described in QueryFactory.
|
||||
*
|
||||
* @return analyzer for query
|
||||
* @see QueryFactory
|
||||
*/
|
||||
public Analyzer getQueryAnalyzer() throws IOException {
|
||||
if (isEmpty(queryAnalyzer)) {
|
||||
try {
|
||||
|
||||
Analyzer defaultAnalyzer = createDefaultAnalyzerBuilder().build();
|
||||
Analyzer genreAnalyzer = createGenreAnalyzerBuilder().build();
|
||||
|
||||
Map<String, Analyzer> fieldAnalyzers = new HashMap<>();
|
||||
fieldAnalyzers.put(FieldNames.GENRE, genreAnalyzer);
|
||||
|
||||
queryAnalyzer = new PerFieldAnalyzerWrapper(defaultAnalyzer, fieldAnalyzers);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Error when initializing Analyzer.", e);
|
||||
}
|
||||
}
|
||||
return queryAnalyzer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+235
-181
@@ -1,181 +1,235 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.Store;
|
||||
import org.apache.lucene.document.FieldType;
|
||||
import org.apache.lucene.document.IntPoint;
|
||||
import org.apache.lucene.document.SortedDocValuesField;
|
||||
import org.apache.lucene.document.StoredField;
|
||||
import org.apache.lucene.document.TextField;
|
||||
import org.apache.lucene.index.IndexOptions;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.util.BytesRef;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
|
||||
/**
|
||||
* A factory that generates the documents to be stored in the index.
|
||||
*/
|
||||
@Component
|
||||
public class DocumentFactory {
|
||||
|
||||
private static final FieldType TYPE_ID;
|
||||
|
||||
private static final FieldType TYPE_ID_NO_STORE;
|
||||
|
||||
private static final FieldType TYPE_KEY;
|
||||
|
||||
static {
|
||||
|
||||
TYPE_ID = new FieldType();
|
||||
TYPE_ID.setIndexOptions(IndexOptions.DOCS);
|
||||
TYPE_ID.setTokenized(false);
|
||||
TYPE_ID.setOmitNorms(true);
|
||||
TYPE_ID.setStored(true);
|
||||
TYPE_ID.freeze();
|
||||
|
||||
TYPE_ID_NO_STORE = new FieldType();
|
||||
TYPE_ID_NO_STORE.setIndexOptions(IndexOptions.DOCS);
|
||||
TYPE_ID_NO_STORE.setTokenized(false);
|
||||
TYPE_ID_NO_STORE.setOmitNorms(true);
|
||||
TYPE_ID_NO_STORE.setStored(false);
|
||||
TYPE_ID_NO_STORE.freeze();
|
||||
|
||||
TYPE_KEY = new FieldType();
|
||||
TYPE_KEY.setIndexOptions(IndexOptions.DOCS);
|
||||
TYPE_KEY.setTokenized(false);
|
||||
TYPE_KEY.setOmitNorms(true);
|
||||
TYPE_KEY.setStored(false);
|
||||
TYPE_KEY.freeze();
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface Consumer<T, U, V> {
|
||||
void accept(T t, U u, V v);
|
||||
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull Integer> fieldId = (doc, value) -> {
|
||||
doc.add(new StoredField(FieldNames.ID, Integer.toString(value), TYPE_ID));
|
||||
};
|
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull Integer> fieldFolderId = (doc, value) -> {
|
||||
doc.add(new StoredField(FieldNames.FOLDER_ID, Integer.toString(value), TYPE_ID_NO_STORE));
|
||||
};
|
||||
|
||||
private Consumer<@NonNull Document, @NonNull String, @NonNull String> fieldKey = (doc, field, value) -> {
|
||||
doc.add(new StoredField(field, value, TYPE_KEY));
|
||||
};
|
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull String> fieldMediatype = (doc, value) ->
|
||||
fieldKey.accept(doc, FieldNames.MEDIA_TYPE, value);
|
||||
|
||||
private BiConsumer<@NonNull Document, @NonNull String> fieldFolderPath = (doc, value) ->
|
||||
fieldKey.accept(doc, FieldNames.FOLDER, value);
|
||||
|
||||
private BiConsumer<@NonNull Document, @Nullable String> fieldGenre = (doc, value) -> {
|
||||
if (isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
fieldKey.accept(doc, FieldNames.GENRE, value);
|
||||
};
|
||||
|
||||
private Consumer<@NonNull Document, @NonNull String, @Nullable Integer> fieldYear = (doc, fieldName, value) -> {
|
||||
if (isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
doc.add(new IntPoint(fieldName, value));
|
||||
};
|
||||
|
||||
private Consumer<@NonNull Document, @NonNull String, @Nullable String> fieldWords = (doc, fieldName, value) -> {
|
||||
if (isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
doc.add(new TextField(fieldName, value, Store.NO));
|
||||
doc.add(new SortedDocValuesField(fieldName, new BytesRef(value)));
|
||||
};
|
||||
|
||||
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()));
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param mediaFile target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createAlbumDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
fieldId.accept(doc, mediaFile.getId());
|
||||
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist());
|
||||
fieldWords.accept(doc, FieldNames.ALBUM, mediaFile.getAlbumName());
|
||||
fieldFolderPath.accept(doc, mediaFile.getFolder());
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param mediaFile target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createArtistDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
fieldId.accept(doc, mediaFile.getId());
|
||||
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist());
|
||||
fieldFolderPath.accept(doc, mediaFile.getFolder());
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param album target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createAlbumId3Document(Album album) {
|
||||
Document doc = new Document();
|
||||
fieldId.accept(doc, album.getId());
|
||||
fieldWords.accept(doc, FieldNames.ARTIST, album.getArtist());
|
||||
fieldWords.accept(doc, FieldNames.ALBUM, album.getName());
|
||||
fieldFolderId.accept(doc, album.getFolderId());
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param artist target of document
|
||||
* @param musicFolder target folder exists
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* Only null check specification of createArtistId3Document is different from legacy.
|
||||
* (The reason is only to simplify the function.)
|
||||
*
|
||||
* Since the field of domain object Album is nonnull,
|
||||
* null check was not performed.
|
||||
*
|
||||
* In implementation ARTIST and ALBUM became nullable,
|
||||
* but null is not input at this point in data flow.
|
||||
*/
|
||||
public Document createArtistId3Document(Artist artist, MusicFolder musicFolder) {
|
||||
Document doc = new Document();
|
||||
fieldId.accept(doc, artist.getId());
|
||||
fieldWords.accept(doc, FieldNames.ARTIST, artist.getName());
|
||||
fieldFolderId.accept(doc, 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();
|
||||
fieldId.accept(doc, mediaFile.getId());
|
||||
fieldMediatype.accept(doc, mediaFile.getMediaType().name());
|
||||
fieldWords.accept(doc, FieldNames.TITLE, mediaFile.getTitle());
|
||||
fieldWords.accept(doc, FieldNames.ARTIST, mediaFile.getArtist());
|
||||
fieldGenre.accept(doc, mediaFile.getGenre());
|
||||
fieldYear.accept(doc, FieldNames.YEAR, mediaFile.getYear());
|
||||
fieldFolderPath.accept(doc, mediaFile.getFolder());
|
||||
return doc;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,95 +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";
|
||||
|
||||
}
|
||||
/*
|
||||
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";
|
||||
|
||||
}
|
||||
|
||||
+323
-169
@@ -1,169 +1,323 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.commons.io.FileUtils;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.SearcherManager;
|
||||
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.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Schema version of Airsonic index.
|
||||
* It may be incremented in the following cases:
|
||||
*
|
||||
* - Incompatible update case in Lucene index implementation
|
||||
* - When schema definition is changed due to modification of AnalyzerFactory,
|
||||
* DocumentFactory or the class that they use.
|
||||
*
|
||||
*/
|
||||
private static final int INDEX_VERSION = 16;
|
||||
|
||||
/**
|
||||
* Literal name of index top directory.
|
||||
*/
|
||||
private static final String INDEX_ROOT_DIR_NAME = "index";
|
||||
|
||||
/**
|
||||
* File supplier for index directory.
|
||||
*/
|
||||
private Supplier<File> rootIndexDirectory = () ->
|
||||
new File(SettingsService.getAirsonicHome(), INDEX_ROOT_DIR_NAME.concat(Integer.toString(INDEX_VERSION)));
|
||||
|
||||
/**
|
||||
* Returns the directory of the specified index
|
||||
*/
|
||||
private Function<IndexType, File> getIndexDirectory = (indexType) ->
|
||||
new File(rootIndexDirectory.get(), indexType.toString().toLowerCase());
|
||||
|
||||
@Autowired
|
||||
private AnalyzerFactory analyzerFactory;
|
||||
|
||||
@Autowired
|
||||
private DocumentFactory documentFactory;
|
||||
|
||||
private Map<IndexType, SearcherManager> searchers = new HashMap<>();
|
||||
|
||||
private Map<IndexType, IndexWriter> writers = new HashMap<>();
|
||||
|
||||
public void index(Album album) {
|
||||
Term primarykey = documentFactory.createPrimarykey(album);
|
||||
Document document = documentFactory.createAlbumId3Document(album);
|
||||
try {
|
||||
writers.get(IndexType.ALBUM_ID3).updateDocument(primarykey, document);
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + album, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(Artist artist, MusicFolder musicFolder) {
|
||||
Term primarykey = documentFactory.createPrimarykey(artist);
|
||||
Document document = documentFactory.createArtistId3Document(artist, musicFolder);
|
||||
try {
|
||||
writers.get(IndexType.ARTIST_ID3).updateDocument(primarykey, document);
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + artist, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(MediaFile mediaFile) {
|
||||
Term primarykey = documentFactory.createPrimarykey(mediaFile);
|
||||
try {
|
||||
if (mediaFile.isFile()) {
|
||||
Document document = documentFactory.createSongDocument(mediaFile);
|
||||
writers.get(IndexType.SONG).updateDocument(primarykey, document);
|
||||
} else if (mediaFile.isAlbum()) {
|
||||
Document document = documentFactory.createAlbumDocument(mediaFile);
|
||||
writers.get(IndexType.ALBUM).updateDocument(primarykey, document);
|
||||
} else {
|
||||
Document document = documentFactory.createArtistDocument(mediaFile);
|
||||
writers.get(IndexType.ARTIST).updateDocument(primarykey, document);
|
||||
}
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + mediaFile, x);
|
||||
}
|
||||
}
|
||||
|
||||
public final void startIndexing() {
|
||||
try {
|
||||
for (IndexType IndexType : IndexType.values()) {
|
||||
writers.put(IndexType, createIndexWriter(IndexType));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create search index.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param indexType
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private IndexWriter createIndexWriter(IndexType indexType) throws IOException {
|
||||
File indexDirectory = getIndexDirectory.apply(indexType);
|
||||
IndexWriterConfig config = new IndexWriterConfig(analyzerFactory.getAnalyzer());
|
||||
return new IndexWriter(FSDirectory.open(indexDirectory.toPath()), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Writer of all indexes and update SearcherManager.
|
||||
* Called at the end of the Scan flow.
|
||||
*/
|
||||
public void stopIndexing() {
|
||||
Arrays.asList(IndexType.values()).forEach(this::stopIndexing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Writer of specified index and refresh SearcherManager.
|
||||
* @param type
|
||||
*/
|
||||
private void stopIndexing(IndexType type) {
|
||||
|
||||
boolean isUpdate = false;
|
||||
// close
|
||||
try {
|
||||
isUpdate = -1 != writers.get(type).commit();
|
||||
writers.get(type).close();
|
||||
writers.remove(type);
|
||||
LOG.trace("Success to create or update search index : [" + type + "]");
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create search index.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(writers.get(type));
|
||||
}
|
||||
|
||||
// refresh reader as index may have been written
|
||||
if (isUpdate && searchers.containsKey(type)) {
|
||||
try {
|
||||
searchers.get(type).maybeRefresh();
|
||||
LOG.trace("SearcherManager has been refreshed : [" + type + "]");
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to refresh SearcherManager : [" + type + "]", e);
|
||||
searchers.remove(type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the IndexSearcher of the specified index.
|
||||
* At initial startup, it may return null
|
||||
* if the user performs any search before performing a scan.
|
||||
*
|
||||
* @param indexType
|
||||
* @return
|
||||
*/
|
||||
public @Nullable IndexSearcher getSearcher(IndexType indexType) {
|
||||
if (!searchers.containsKey(indexType)) {
|
||||
File indexDirectory = getIndexDirectory.apply(indexType);
|
||||
try {
|
||||
if (indexDirectory.exists()) {
|
||||
SearcherManager manager = new SearcherManager(FSDirectory.open(indexDirectory.toPath()), null);
|
||||
searchers.put(indexType, manager);
|
||||
} else {
|
||||
LOG.warn("{} does not exist. Please run a scan.", indexDirectory.getAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to initialize SearcherManager.", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
SearcherManager manager = searchers.get(indexType);
|
||||
if (!isEmpty(manager)) {
|
||||
return searchers.get(indexType).acquire();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to acquire IndexSearcher.", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void release(IndexType indexType, IndexSearcher indexSearcher) {
|
||||
if (searchers.containsKey(indexType)) {
|
||||
try {
|
||||
searchers.get(indexType).release(indexSearcher);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to release IndexSearcher.", e);
|
||||
searchers.remove(indexType);
|
||||
}
|
||||
} else {
|
||||
// irregular case
|
||||
try {
|
||||
indexSearcher.getIndexReader().close();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to release. IndexSearcher has been closed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the version of the index and clean it up if necessary.
|
||||
* Legacy type indexes (files or directories starting with lucene) are deleted.
|
||||
* If there is no index directory, initialize the directory.
|
||||
* If the index directory exists and is not the current version,
|
||||
* initialize the directory.
|
||||
*/
|
||||
public void deleteOldIndexFiles() {
|
||||
|
||||
// Delete legacy files unconditionally
|
||||
Arrays.stream(SettingsService.getAirsonicHome()
|
||||
.listFiles((file, name) -> Pattern.compile("^lucene\\d+$").matcher(name).matches())).forEach(old -> {
|
||||
if (FileUtil.exists(old)) {
|
||||
LOG.info("Found legacy index file. Try to delete : {}", old.getAbsolutePath());
|
||||
try {
|
||||
if (old.isFile()) {
|
||||
FileUtils.deleteQuietly(old);
|
||||
} else {
|
||||
FileUtils.deleteDirectory(old);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Log only if failed
|
||||
LOG.warn("Failed to delete the legacy Index : ".concat(old.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Delete if not old index version
|
||||
Arrays.stream(SettingsService.getAirsonicHome()
|
||||
.listFiles((file, name) -> Pattern.compile("^index\\d+$").matcher(name).matches()))
|
||||
.filter(dir -> !dir.getName().equals(rootIndexDirectory.get().getName()))
|
||||
.forEach(old -> {
|
||||
if (FileUtil.exists(old)) {
|
||||
LOG.info("Found old index file. Try to delete : {}", old.getAbsolutePath());
|
||||
try {
|
||||
if (old.isFile()) {
|
||||
FileUtils.deleteQuietly(old);
|
||||
} else {
|
||||
FileUtils.deleteDirectory(old);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Log only if failed
|
||||
LOG.warn("Failed to delete the old Index : ".concat(old.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory corresponding to the current index version.
|
||||
*/
|
||||
public void initializeIndexDirectory() {
|
||||
// Check if Index is current version
|
||||
if (rootIndexDirectory.get().exists()) {
|
||||
// Index of current version already exists
|
||||
LOG.info("Index was found (index version {}). ", INDEX_VERSION);
|
||||
} else {
|
||||
if (rootIndexDirectory.get().mkdir()) {
|
||||
LOG.info("Index directory was created (index version {}). ", INDEX_VERSION);
|
||||
} else {
|
||||
LOG.warn("Failed to create index directory : (index version {}). ", INDEX_VERSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,137 +1,142 @@
|
||||
/*
|
||||
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 java.util.AbstractMap;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Enum that symbolizes the each lucene index entity.
|
||||
* This class is a division of what was once part of SearchService and added functionality.
|
||||
* @since legacy
|
||||
*/
|
||||
public enum IndexType {
|
||||
|
||||
/*
|
||||
* Boosts is a factor for search scores, which is 1 by default.
|
||||
*/
|
||||
SONG(
|
||||
fieldNames(
|
||||
FieldNames.TITLE,
|
||||
FieldNames.ARTIST),
|
||||
boosts(
|
||||
entry(FieldNames.TITLE, 2F))),
|
||||
|
||||
ALBUM(
|
||||
fieldNames(
|
||||
FieldNames.ALBUM,
|
||||
FieldNames.ARTIST,
|
||||
FieldNames.FOLDER),
|
||||
boosts(
|
||||
entry(FieldNames.ALBUM, 2F))),
|
||||
|
||||
ALBUM_ID3(
|
||||
fieldNames(
|
||||
FieldNames.ALBUM,
|
||||
FieldNames.ARTIST,
|
||||
FieldNames.FOLDER_ID),
|
||||
boosts(
|
||||
entry(FieldNames.ALBUM, 2F))),
|
||||
|
||||
ARTIST(
|
||||
fieldNames(
|
||||
FieldNames.ARTIST,
|
||||
FieldNames.FOLDER),
|
||||
boosts(
|
||||
entry(FieldNames.ARTIST, 1F))),
|
||||
|
||||
ARTIST_ID3(
|
||||
fieldNames(
|
||||
FieldNames.ARTIST),
|
||||
boosts(
|
||||
entry(FieldNames.ARTIST, 2F))),
|
||||
;
|
||||
|
||||
@SafeVarargs
|
||||
private static final Map<String, Float> boosts(SimpleEntry<String, Float>... entry) {
|
||||
Map<String, Float> m = new HashMap<>();
|
||||
Arrays.stream(entry).forEach(kv -> m.put(kv.getKey(), kv.getValue()));
|
||||
return Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
/*
|
||||
* The current state is implemented to set the same value as legacy.
|
||||
* However unlike legacy, it has been changed
|
||||
* so that different values can be set for each field.
|
||||
* When setting two or more boost values,
|
||||
* it is desirable to differentiate the values.
|
||||
*/
|
||||
private static final SimpleEntry<String, Float> entry(String k, float v) {
|
||||
return new AbstractMap.SimpleEntry<>(k, v);
|
||||
}
|
||||
|
||||
private static final String[] fieldNames(String... names) {
|
||||
return Arrays.stream(names).toArray(String[]::new);
|
||||
}
|
||||
|
||||
private final Map<String, Float> boosts;
|
||||
|
||||
private final String[] fields;
|
||||
|
||||
private IndexType(String[] fieldNames, Map<String, Float> boosts) {
|
||||
this.fields = fieldNames;
|
||||
this.boosts = boosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of fields and boost values.
|
||||
*
|
||||
* @return map of fields and boost values
|
||||
* @since legacy
|
||||
*/
|
||||
/*
|
||||
* See the lucene documentation for boost specifications.
|
||||
*/
|
||||
public Map<String, Float> getBoosts() {
|
||||
return boosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return some of the fields defined in the index.
|
||||
*
|
||||
* @return Fields mainly used in multi-field search
|
||||
* @since legacy
|
||||
*/
|
||||
/*
|
||||
* It maintains a fairly early implementation
|
||||
* and can be considered as an argument of MultiFieldQueryParser.
|
||||
* In fact, the fields and boosts used in the search are difficult topics
|
||||
* that can be determined by the search requirements.
|
||||
*/
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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 java.util.AbstractMap;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Enum that symbolizes the each lucene index entity.
|
||||
* This class is a division of what was once part of SearchService and added functionality.
|
||||
* @since legacy
|
||||
*/
|
||||
public enum IndexType {
|
||||
|
||||
SONG(
|
||||
fieldNames(
|
||||
FieldNames.TITLE,
|
||||
FieldNames.ARTIST),
|
||||
boosts(
|
||||
entry(FieldNames.TITLE, 2F))),
|
||||
|
||||
ALBUM(
|
||||
fieldNames(
|
||||
FieldNames.ALBUM,
|
||||
FieldNames.ARTIST),
|
||||
// FieldNames.FOLDER), // XXX 3.x -> 8.x : Remove folder from multi-field search condition
|
||||
boosts(
|
||||
entry(FieldNames.ALBUM, 2F))),
|
||||
|
||||
ALBUM_ID3(
|
||||
fieldNames(
|
||||
FieldNames.ALBUM,
|
||||
FieldNames.ARTIST),
|
||||
// FieldNames.FOLDER_ID), // XXX 3.x -> 8.x : Remove folder from multi-field search condition
|
||||
boosts(
|
||||
entry(FieldNames.ALBUM, 2F))),
|
||||
|
||||
ARTIST(
|
||||
fieldNames(
|
||||
FieldNames.ARTIST),
|
||||
// FieldNames.FOLDER), // XXX 3.x -> 8.x : Remove folder from multi-field search condition
|
||||
boosts(
|
||||
entry(FieldNames.ARTIST, 1F))),
|
||||
|
||||
ARTIST_ID3(
|
||||
fieldNames(
|
||||
FieldNames.ARTIST),
|
||||
boosts(
|
||||
entry(FieldNames.ARTIST, 2F))),
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* Define the field's applied boost value when searching IndexType.
|
||||
*
|
||||
* @param entry {@link #entry(String, float)}.
|
||||
* When specifying multiple values, enumerate entries.
|
||||
* @return Map of boost values to be applied to the field
|
||||
*/
|
||||
@SafeVarargs
|
||||
private static final Map<String, Float> boosts(SimpleEntry<String, Float>... entry) {
|
||||
Map<String, Float> m = new HashMap<>();
|
||||
Arrays.stream(entry).forEach(kv -> m.put(kv.getKey(), kv.getValue()));
|
||||
return Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an entry representing the boost value for the field.
|
||||
*
|
||||
* @param k Field name defined by FieldNames
|
||||
* @param v Boost value
|
||||
* @return
|
||||
*/
|
||||
private static final SimpleEntry<String, Float> entry(String k, float v) {
|
||||
return new AbstractMap.SimpleEntry<>(k, v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the field that the input value is to search for
|
||||
* when searching IndexType.
|
||||
* If you specify multiple values, list the field names.
|
||||
*
|
||||
* @param names
|
||||
* @return
|
||||
*/
|
||||
private static final String[] fieldNames(String... names) {
|
||||
return Arrays.stream(names).toArray(String[]::new);
|
||||
}
|
||||
|
||||
private final Map<String, Float> boosts;
|
||||
|
||||
private final String[] fields;
|
||||
|
||||
private IndexType(String[] fieldNames, Map<String, Float> boosts) {
|
||||
this.fields = fieldNames;
|
||||
this.boosts = boosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of fields and boost values.
|
||||
*
|
||||
* @return Map of fields and boost values
|
||||
* @since legacy
|
||||
* @see BoostQuery
|
||||
*/
|
||||
public Map<String, Float> getBoosts() {
|
||||
return boosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return some of the fields defined in the index.
|
||||
*
|
||||
* @return Fields mainly used in multi-field search
|
||||
* @since legacy
|
||||
*/
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+291
-231
@@ -1,231 +1,291 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.MediaType;
|
||||
import org.airsonic.player.domain.MusicFolder;
|
||||
import org.airsonic.player.domain.RandomSearchCriteria;
|
||||
import org.airsonic.player.domain.SearchCriteria;
|
||||
import org.apache.lucene.analysis.Analyzer;
|
||||
import org.apache.lucene.analysis.TokenStream;
|
||||
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
|
||||
import org.apache.lucene.document.IntPoint;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.search.BooleanClause.Occur;
|
||||
import org.apache.lucene.search.BooleanQuery;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.TermQuery;
|
||||
import org.apache.lucene.search.WildcardQuery;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private static final String ASTERISK = "*";
|
||||
|
||||
@Autowired
|
||||
private AnalyzerFactory analyzerFactory;
|
||||
|
||||
private final Function<MusicFolder, Query> toFolderIdQuery = (folder) -> {
|
||||
// Unanalyzed field
|
||||
return new TermQuery(new Term(FieldNames.FOLDER_ID, folder.getId().toString()));
|
||||
};
|
||||
|
||||
private final Function<MusicFolder, Query> toFolderPathQuery = (folder) -> {
|
||||
// Unanalyzed field
|
||||
return new TermQuery(new Term(FieldNames.FOLDER, folder.getPath().getPath()));
|
||||
};
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* "SpanOr" has been changed to "Or".
|
||||
* - Path comparison is more appropriate with "Or".
|
||||
* - If "SpanOr" is maintained, the DOC design needs to be changed.
|
||||
*/
|
||||
private final BiFunction<@NonNull Boolean, @NonNull List<MusicFolder>, @NonNull Query> toFolderQuery = (
|
||||
isId3, folders) -> {
|
||||
BooleanQuery.Builder mfQuery = new BooleanQuery.Builder();
|
||||
folders.stream()
|
||||
.map(isId3 ? toFolderIdQuery : toFolderPathQuery)
|
||||
.forEach(t -> mfQuery.add(t, Occur.SHOULD));
|
||||
return mfQuery.build();
|
||||
};
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* In order to support wildcards,
|
||||
* MultiFieldQueryParser has been replaced by the following process.
|
||||
*
|
||||
* - There is also an override of MultiFieldQueryParser, but it is known to be high cost.
|
||||
* - MultiFieldQueryParser was created before Java API was modernized.
|
||||
* - The spec of Parser has changed from time to time. Using parser does not reduce library update risk.
|
||||
* - Self made parser process reduces one library dependency.
|
||||
* - It is easy to make corrections later when changing the query to improve search accuracy.
|
||||
*/
|
||||
private Query createMultiFieldWildQuery(@NonNull String[] fieldNames, @NonNull String queryString)
|
||||
throws IOException {
|
||||
|
||||
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder();
|
||||
|
||||
List<List<Query>> fieldsQuerys = new ArrayList<>();
|
||||
Analyzer analyzer = analyzerFactory.getQueryAnalyzer();
|
||||
|
||||
/* Wildcard applies to all tokens. **/
|
||||
for (String fieldName : fieldNames) {
|
||||
try (TokenStream stream = analyzer.tokenStream(fieldName, queryString)) {
|
||||
stream.reset();
|
||||
List<Query> fieldQuerys = new ArrayList<>();
|
||||
while (stream.incrementToken()) {
|
||||
String token = stream.getAttribute(CharTermAttribute.class).toString();
|
||||
WildcardQuery wildcardQuery = new WildcardQuery(new Term(fieldName, token.concat(ASTERISK)));
|
||||
fieldQuerys.add(wildcardQuery);
|
||||
}
|
||||
fieldsQuerys.add(fieldQuerys);
|
||||
}
|
||||
}
|
||||
|
||||
/* If Field's Tokenizer is different, token's length may not match. **/
|
||||
int maxTermLength = fieldsQuerys.stream()
|
||||
.map(l -> l.size())
|
||||
.max(Integer::compare).get();
|
||||
|
||||
if (0 < fieldsQuerys.size()) {
|
||||
for (int i = 0; i < maxTermLength; i++) {
|
||||
BooleanQuery.Builder fieldsQuery = new BooleanQuery.Builder();
|
||||
for (List<Query> fieldQuerys : fieldsQuerys) {
|
||||
if (i < fieldQuerys.size()) {
|
||||
fieldsQuery.add(fieldQuerys.get(i), Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
mainQuery.add(fieldsQuery.build(), Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
|
||||
return mainQuery.build();
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* RangeQuery has been changed to not allow null.
|
||||
*/
|
||||
private final BiFunction<@Nullable Integer, @Nullable Integer, @NonNull Query> toYearRangeQuery =
|
||||
(from, to) -> {
|
||||
return IntPoint.newRangeQuery(FieldNames.YEAR,
|
||||
isEmpty(from) ? Integer.MIN_VALUE : from,
|
||||
isEmpty(to) ? Integer.MAX_VALUE : to);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders,
|
||||
IndexType indexType) throws IOException {
|
||||
|
||||
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder();
|
||||
|
||||
Query multiFieldQuery = createMultiFieldWildQuery(indexType.getFields(), criteria.getQuery());
|
||||
mainQuery.add(multiFieldQuery, Occur.MUST);
|
||||
|
||||
boolean isId3 = indexType == IndexType.ALBUM_ID3 || indexType == IndexType.ARTIST_ID3;
|
||||
Query folderQuery = toFolderQuery.apply(isId3, musicFolders);
|
||||
mainQuery.add(folderQuery, Occur.MUST);
|
||||
|
||||
return mainQuery.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generation expression extracted from
|
||||
* {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}.
|
||||
*
|
||||
* @param criteria criteria
|
||||
* @return Query
|
||||
* @throws IOException
|
||||
*/
|
||||
public Query getRandomSongs(RandomSearchCriteria criteria) throws IOException {
|
||||
|
||||
BooleanQuery.Builder query = new BooleanQuery.Builder();
|
||||
|
||||
Analyzer analyzer = analyzerFactory.getQueryAnalyzer();
|
||||
|
||||
// Unanalyzed field
|
||||
query.add(new TermQuery(new Term(FieldNames.MEDIA_TYPE, MediaType.MUSIC.name())), Occur.MUST);
|
||||
|
||||
if (!isEmpty(criteria.getGenre())) {
|
||||
|
||||
// Unanalyzed field, but performs filtering according to id3 tag parser.
|
||||
try (TokenStream stream = analyzer.tokenStream(FieldNames.GENRE, criteria.getGenre())) {
|
||||
stream.reset();
|
||||
if (stream.incrementToken()) {
|
||||
String token = stream.getAttribute(CharTermAttribute.class).toString();
|
||||
query.add(new TermQuery(new Term(FieldNames.GENRE, token)), Occur.MUST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(isEmpty(criteria.getFromYear()) && isEmpty(criteria.getToYear()))) {
|
||||
query.add(toYearRangeQuery.apply(criteria.getFromYear(), criteria.getToYear()), Occur.MUST);
|
||||
}
|
||||
|
||||
query.add(toFolderQuery.apply(false, criteria.getMusicFolders()), Occur.MUST);
|
||||
|
||||
return query.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 IOException When parsing of QueryParser fails
|
||||
*/
|
||||
public Query searchByName(String fieldName, String name) throws IOException {
|
||||
|
||||
BooleanQuery.Builder mainQuery = new BooleanQuery.Builder();
|
||||
|
||||
Analyzer analyzer = analyzerFactory.getQueryAnalyzer();
|
||||
|
||||
try (TokenStream stream = analyzer.tokenStream(fieldName, name)) {
|
||||
stream.reset();
|
||||
stream.incrementToken();
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* In order to support wildcards,
|
||||
* QueryParser has been replaced by the following process.
|
||||
*/
|
||||
|
||||
/* Wildcards apply only to tail tokens **/
|
||||
while (true) {
|
||||
String token = stream.getAttribute(CharTermAttribute.class).toString();
|
||||
if (stream.incrementToken()) {
|
||||
mainQuery.add(new TermQuery(new Term(fieldName, token)), Occur.SHOULD);
|
||||
} else {
|
||||
WildcardQuery wildcardQuery = new WildcardQuery(new Term(fieldName, token.concat(ASTERISK)));
|
||||
mainQuery.add(wildcardQuery, Occur.SHOULD);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return mainQuery.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return new BooleanQuery.Builder()
|
||||
.add(toFolderQuery.apply(false, musicFolders), Occur.SHOULD)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return new BooleanQuery.Builder()
|
||||
.add(toFolderQuery.apply(true, musicFolders), Occur.SHOULD)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+54
-107
@@ -22,14 +22,8 @@ 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;
|
||||
@@ -59,35 +53,6 @@ public class SearchServiceImpl implements SearchService {
|
||||
// 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) {
|
||||
@@ -100,26 +65,28 @@ public class SearchServiceImpl implements SearchService {
|
||||
if (count <= 0)
|
||||
return result;
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
IndexSearcher searcher = indexManager.getSearcher(indexType);
|
||||
if (isEmpty(searcher)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
reader = indexManager.createIndexReader(indexType);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
try {
|
||||
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);
|
||||
TopDocs topDocs = searcher.search(query, offset + count);
|
||||
int totalHits = util.round.apply(topDocs.totalHits.value);
|
||||
result.setTotalHits(totalHits);
|
||||
int start = Math.min(offset, totalHits);
|
||||
int end = Math.min(start + count, totalHits);
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
util.addIfAnyMatch(result, indexType, searcher.doc(topDocs.scoreDocs[i].doc));
|
||||
}
|
||||
|
||||
} catch (IOException | ParseException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to execute Lucene search.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
indexManager.release(indexType, searcher);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -135,7 +102,7 @@ public class SearchServiceImpl implements SearchService {
|
||||
* @throws IOException
|
||||
*/
|
||||
private final <D> List<D> createRandomDocsList(
|
||||
int count, Searcher searcher, Query query, BiConsumer<List<D>, Integer> id2ListCallBack)
|
||||
int count, IndexSearcher searcher, Query query, BiConsumer<List<D>, Integer> id2ListCallBack)
|
||||
throws IOException {
|
||||
|
||||
List<Integer> docs = Arrays
|
||||
@@ -157,14 +124,13 @@ public class SearchServiceImpl implements SearchService {
|
||||
@Override
|
||||
public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) {
|
||||
|
||||
IndexReader reader = null;
|
||||
IndexSearcher searcher = indexManager.getSearcher(SONG);
|
||||
if (isEmpty(searcher)) {
|
||||
// At first start
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -173,7 +139,7 @@ public class SearchServiceImpl implements SearchService {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to search or random songs.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
indexManager.release(IndexType.SONG, searcher);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -181,22 +147,22 @@ public class SearchServiceImpl implements SearchService {
|
||||
@Override
|
||||
public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) {
|
||||
|
||||
IndexReader reader = null;
|
||||
IndexSearcher searcher = indexManager.getSearcher(IndexType.ALBUM);
|
||||
if (isEmpty(searcher)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Query query = queryFactory.getRandomAlbums(musicFolders);
|
||||
|
||||
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);
|
||||
indexManager.release(IndexType.ALBUM, searcher);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -204,22 +170,22 @@ public class SearchServiceImpl implements SearchService {
|
||||
@Override
|
||||
public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) {
|
||||
|
||||
IndexReader reader = null;
|
||||
IndexSearcher searcher = indexManager.getSearcher(IndexType.ALBUM_ID3);
|
||||
if (isEmpty(searcher)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Query query = queryFactory.getRandomAlbumsId3(musicFolders);
|
||||
|
||||
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);
|
||||
indexManager.release(IndexType.ALBUM_ID3, searcher);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -241,58 +207,39 @@ public class SearchServiceImpl implements SearchService {
|
||||
return result;
|
||||
}
|
||||
|
||||
IndexReader reader = null;
|
||||
IndexSearcher searcher = indexManager.getSearcher(indexType);
|
||||
if (isEmpty(searcher)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
SortField[] sortFields = Arrays
|
||||
.stream(indexType.getFields())
|
||||
.map(n -> new SortField(n, SortField.Type.STRING))
|
||||
.toArray(i -> new SortField[i]);
|
||||
Sort sort = new Sort(sortFields);
|
||||
|
||||
result.setTotalHits(topDocs.totalHits);
|
||||
int start = Math.min(offset, topDocs.totalHits);
|
||||
int end = Math.min(start + count, topDocs.totalHits);
|
||||
TopDocs topDocs = searcher.search(query, offset + count, sort);
|
||||
|
||||
int totalHits = util.round.apply(topDocs.totalHits.value);
|
||||
result.setTotalHits(totalHits);
|
||||
int start = Math.min(offset, totalHits);
|
||||
int end = Math.min(start + count, 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) {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to execute Lucene search.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
indexManager.release(indexType, searcher);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+176
-200
@@ -1,200 +1,176 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
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.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+137
-137
@@ -1,138 +1,138 @@
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.airsonic.player.TestCaseUtils;
|
||||
import org.airsonic.player.dao.DaoHelper;
|
||||
import org.airsonic.player.dao.MusicFolderDao;
|
||||
import org.airsonic.player.service.MediaScannerService;
|
||||
import org.airsonic.player.service.SettingsService;
|
||||
import org.airsonic.player.util.HomeRule;
|
||||
import org.airsonic.player.util.MusicFolderTestData;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
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.stereotype.Component;
|
||||
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;
|
||||
|
||||
@ContextConfiguration(locations = {
|
||||
"/applicationContext-service.xml",
|
||||
"/applicationContext-cache.xml",
|
||||
"/applicationContext-testdb.xml"})
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
@Component
|
||||
/**
|
||||
* Abstract class for scanning MusicFolder.
|
||||
*/
|
||||
public abstract class AbstractAirsonicHomeTest implements AirsonicHomeTest {
|
||||
|
||||
@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);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Currently, Maven is executing test classes in series,
|
||||
* so this class can hold the state.
|
||||
* When executing in parallel, subclasses should override this.
|
||||
*/
|
||||
private static AtomicBoolean dataBasePopulated = new AtomicBoolean();
|
||||
|
||||
// Above.
|
||||
private static AtomicBoolean dataBaseReady = new AtomicBoolean();
|
||||
|
||||
protected final static Function<String, String> resolveBaseMediaPath = (childPath) -> {
|
||||
return MusicFolderTestData.resolveBaseMediaPath().concat(childPath);
|
||||
};
|
||||
|
||||
@Autowired
|
||||
protected DaoHelper daoHelper;
|
||||
|
||||
@Autowired
|
||||
protected MediaScannerService mediaScannerService;
|
||||
|
||||
@Autowired
|
||||
protected MusicFolderDao musicFolderDao;
|
||||
|
||||
@Autowired
|
||||
protected SettingsService settingsService;
|
||||
|
||||
@Rule
|
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Override
|
||||
public AtomicBoolean dataBasePopulated() {
|
||||
return dataBasePopulated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtomicBoolean dataBaseReady() {
|
||||
return dataBaseReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void populateDatabaseOnlyOnce() {
|
||||
if (!dataBasePopulated().get()) {
|
||||
dataBasePopulated().set(true);
|
||||
getMusicFolders().forEach(musicFolderDao::createMusicFolder);
|
||||
settingsService.clearMusicFolderCache();
|
||||
try {
|
||||
// Await time to avoid scan failure.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
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")
|
||||
| s.equals("ARTIST")
|
||||
| s.equals("MUSIC_FOLDER")
|
||||
| s.equals("ALBUM"))
|
||||
.forEach(tableName ->
|
||||
System.out.println("\t" + tableName + " : " + records.get(tableName).toString()));
|
||||
System.out.println("--- *********************** ---");
|
||||
try {
|
||||
// Await for Lucene to finish writing(asynchronous).
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
dataBaseReady().set(true);
|
||||
} else {
|
||||
while (!dataBaseReady().get()) {
|
||||
try {
|
||||
// The subsequent test method waits while reading DB data.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.airsonic.player.TestCaseUtils;
|
||||
import org.airsonic.player.dao.DaoHelper;
|
||||
import org.airsonic.player.dao.MusicFolderDao;
|
||||
import org.airsonic.player.service.MediaScannerService;
|
||||
import org.airsonic.player.service.SettingsService;
|
||||
import org.airsonic.player.util.HomeRule;
|
||||
import org.airsonic.player.util.MusicFolderTestData;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
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.stereotype.Component;
|
||||
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;
|
||||
|
||||
@ContextConfiguration(locations = {
|
||||
"/applicationContext-service.xml",
|
||||
"/applicationContext-cache.xml",
|
||||
"/applicationContext-testdb.xml"})
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
@Component
|
||||
/**
|
||||
* Abstract class for scanning MusicFolder.
|
||||
*/
|
||||
public abstract class AbstractAirsonicHomeTest implements AirsonicHomeTest {
|
||||
|
||||
@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);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Currently, Maven is executing test classes in series,
|
||||
* so this class can hold the state.
|
||||
* When executing in parallel, subclasses should override this.
|
||||
*/
|
||||
private static AtomicBoolean dataBasePopulated = new AtomicBoolean();
|
||||
|
||||
// Above.
|
||||
private static AtomicBoolean dataBaseReady = new AtomicBoolean();
|
||||
|
||||
protected final static Function<String, String> resolveBaseMediaPath = (childPath) -> {
|
||||
return MusicFolderTestData.resolveBaseMediaPath().concat(childPath);
|
||||
};
|
||||
|
||||
@Autowired
|
||||
protected DaoHelper daoHelper;
|
||||
|
||||
@Autowired
|
||||
protected MediaScannerService mediaScannerService;
|
||||
|
||||
@Autowired
|
||||
protected MusicFolderDao musicFolderDao;
|
||||
|
||||
@Autowired
|
||||
protected SettingsService settingsService;
|
||||
|
||||
@Rule
|
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Override
|
||||
public AtomicBoolean dataBasePopulated() {
|
||||
return dataBasePopulated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AtomicBoolean dataBaseReady() {
|
||||
return dataBaseReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void populateDatabaseOnlyOnce() {
|
||||
if (!dataBasePopulated().get()) {
|
||||
dataBasePopulated().set(true);
|
||||
getMusicFolders().forEach(musicFolderDao::createMusicFolder);
|
||||
settingsService.clearMusicFolderCache();
|
||||
try {
|
||||
// Await time to avoid scan failure.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
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")
|
||||
| s.equals("ARTIST")
|
||||
| s.equals("MUSIC_FOLDER")
|
||||
| s.equals("ALBUM"))
|
||||
.forEach(tableName ->
|
||||
System.out.println("\t" + tableName + " : " + records.get(tableName).toString()));
|
||||
System.out.println("--- *********************** ---");
|
||||
try {
|
||||
// Await for Lucene to finish writing(asynchronous).
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
dataBaseReady().set(true);
|
||||
} else {
|
||||
while (!dataBaseReady().get()) {
|
||||
try {
|
||||
// The subsequent test method waits while reading DB data.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+45
-45
@@ -1,45 +1,45 @@
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.airsonic.player.domain.MusicFolder;
|
||||
import org.airsonic.player.util.MusicFolderTestData;
|
||||
|
||||
/**
|
||||
* Test case interface for scanning MusicFolder.
|
||||
*/
|
||||
public interface AirsonicHomeTest {
|
||||
|
||||
/**
|
||||
* MusicFolder used by test class.
|
||||
*
|
||||
* @return MusicFolder used by test class
|
||||
*/
|
||||
default List<MusicFolder> getMusicFolders() {
|
||||
return MusicFolderTestData.getTestMusicFolders();
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the data input has been completed.
|
||||
*
|
||||
* @return Static AtomicBoolean indicating whether the data injection has been
|
||||
* completed
|
||||
*/
|
||||
abstract AtomicBoolean dataBasePopulated();
|
||||
|
||||
/**
|
||||
* Whether the data input has been completed.
|
||||
*
|
||||
* @return Static AtomicBoolean indicating whether the data injection has been
|
||||
* completed
|
||||
*/
|
||||
abstract AtomicBoolean dataBaseReady();
|
||||
|
||||
/**
|
||||
* Populate the database only once.
|
||||
* It is called in the @Before granted method.
|
||||
*/
|
||||
void populateDatabaseOnlyOnce();
|
||||
|
||||
}
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.airsonic.player.domain.MusicFolder;
|
||||
import org.airsonic.player.util.MusicFolderTestData;
|
||||
|
||||
/**
|
||||
* Test case interface for scanning MusicFolder.
|
||||
*/
|
||||
public interface AirsonicHomeTest {
|
||||
|
||||
/**
|
||||
* MusicFolder used by test class.
|
||||
*
|
||||
* @return MusicFolder used by test class
|
||||
*/
|
||||
default List<MusicFolder> getMusicFolders() {
|
||||
return MusicFolderTestData.getTestMusicFolders();
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the data input has been completed.
|
||||
*
|
||||
* @return Static AtomicBoolean indicating whether the data injection has been
|
||||
* completed
|
||||
*/
|
||||
abstract AtomicBoolean dataBasePopulated();
|
||||
|
||||
/**
|
||||
* Whether the data input has been completed.
|
||||
*
|
||||
* @return Static AtomicBoolean indicating whether the data injection has been
|
||||
* completed
|
||||
*/
|
||||
abstract AtomicBoolean dataBaseReady();
|
||||
|
||||
/**
|
||||
* Populate the database only once.
|
||||
* It is called in the @Before granted method.
|
||||
*/
|
||||
void populateDatabaseOnlyOnce();
|
||||
|
||||
}
|
||||
|
||||
+73
-31
@@ -11,7 +11,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.lucene.analysis.TokenStream;
|
||||
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
|
||||
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -85,9 +85,12 @@ public class AnalyzerFactoryTestCase {
|
||||
public void testPunctuation1() {
|
||||
|
||||
String query = "B︴C";
|
||||
String expected1 = "b";
|
||||
String expected2 = "c";
|
||||
String expected = "b︴c";
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* The definition of punctuation has changed.
|
||||
*/
|
||||
Arrays.stream(IndexType.values()).flatMap(i -> Arrays.stream(i.getFields())).forEach(n -> {
|
||||
List<String> terms = toTermString(n, query);
|
||||
switch (n) {
|
||||
@@ -100,9 +103,8 @@ public class AnalyzerFactoryTestCase {
|
||||
case FieldNames.FOLDER:
|
||||
case FieldNames.GENRE:
|
||||
case FieldNames.MEDIA_TYPE:
|
||||
assertEquals("tokenized : " + n, 2, terms.size());
|
||||
assertEquals("tokenized : " + n, expected1, terms.get(0));
|
||||
assertEquals("tokenized : " + n, expected2, terms.get(1));
|
||||
assertEquals("tokenized : " + n, 1, terms.size());
|
||||
assertEquals("tokenized : " + n, expected, terms.get(0));
|
||||
break;
|
||||
|
||||
/*
|
||||
@@ -112,9 +114,8 @@ public class AnalyzerFactoryTestCase {
|
||||
case FieldNames.ARTIST:
|
||||
case FieldNames.ALBUM:
|
||||
case FieldNames.TITLE:
|
||||
assertEquals("tokenized : " + n, 2, terms.size());
|
||||
assertEquals("tokenized : " + n, expected1, terms.get(0));
|
||||
assertEquals("tokenized : " + n, expected2, terms.get(1));
|
||||
assertEquals("tokenized : " + n, 1, terms.size());
|
||||
assertEquals("tokenized : " + n, expected, terms.get(0));
|
||||
break;
|
||||
/*
|
||||
* ID, FOLDER_ID, YEAR
|
||||
@@ -256,12 +257,17 @@ public class AnalyzerFactoryTestCase {
|
||||
*/
|
||||
String queryFullWidth = "THIS IS FULL-WIDTH SENTENCES.";
|
||||
terms = toTermString(queryFullWidth);
|
||||
assertEquals(5, terms.size());
|
||||
assertEquals("this", terms.get(0));// removal target is ignored
|
||||
assertEquals("is", terms.get(1));
|
||||
assertEquals("full", terms.get(2));
|
||||
assertEquals("width", terms.get(3));
|
||||
assertEquals("sentences", terms.get(4));
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
*
|
||||
* This is not a change due to the library but an intentional change.
|
||||
* The filter order has been changed properly
|
||||
* as it is probably not a deliberate specification.
|
||||
*/
|
||||
assertEquals(3, terms.size());
|
||||
assertEquals("full", terms.get(0));
|
||||
assertEquals("width", terms.get(1));
|
||||
assertEquals("sentences", terms.get(2));
|
||||
|
||||
}
|
||||
|
||||
@@ -428,6 +434,11 @@ public class AnalyzerFactoryTestCase {
|
||||
query = "_ID3_ARTIST_ Céline Frisch: Café Zimmermann";
|
||||
terms = toTermString(query);
|
||||
assertEquals(5, terms.size());
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x : _id3_artist_ in UAX#29.
|
||||
* Since the effect is large, trim with Filter.
|
||||
*/
|
||||
assertEquals("id3_artist", terms.get(0));
|
||||
assertEquals("celine", terms.get(1));
|
||||
assertEquals("frisch", terms.get(2));
|
||||
@@ -438,6 +449,11 @@ public class AnalyzerFactoryTestCase {
|
||||
query = "_ID3_ARTIST_ Sarah Walker/Nash Ensemble";
|
||||
terms = toTermString(query);
|
||||
assertEquals(5, terms.size());
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x : _id3_artist_ in UAX#29.
|
||||
* Since the effect is large, trim with Filter.
|
||||
*/
|
||||
assertEquals("id3_artist", terms.get(0));
|
||||
assertEquals("sarah", terms.get(1));
|
||||
assertEquals("walker", terms.get(2));
|
||||
@@ -464,14 +480,18 @@ public class AnalyzerFactoryTestCase {
|
||||
assertEquals(asList("abc", "def"), toTermString("~ABC~DEF~"));
|
||||
assertEquals(asList("abc", "def"), toTermString("*ABC*DEF*"));
|
||||
assertEquals(asList("abc", "def"), toTermString("?ABC?DEF?"));
|
||||
assertEquals(asList("abc", "def"), toTermString(":ABC:DEF:"));
|
||||
assertEquals(asList("abc:def"), toTermString(":ABC:DEF:")); // XXX 3.x -> 8.x : abc def -> abc:def
|
||||
assertEquals(asList("abc", "def"), toTermString("-ABC-DEF-"));
|
||||
assertEquals(asList("abc", "def"), toTermString("/ABC/DEF/"));
|
||||
assertEquals(asList("abc", "def"), toTermString("_ABC_DEF_"));
|
||||
/*
|
||||
* XXX 3.x -> 8.x : _abc_def_ in UAX#29.
|
||||
* Since the effect is large, trim with Filter.
|
||||
*/
|
||||
assertEquals(asList("abc_def"), toTermString("_ABC_DEF_")); // XXX 3.x -> 8.x : abc def -> abc_def
|
||||
assertEquals(asList("abc", "def"), toTermString(",ABC,DEF,"));
|
||||
assertEquals(asList("abc.def"), toTermString(".ABC.DEF."));
|
||||
assertEquals(asList("abc&def"), toTermString("&ABC&DEF&"));
|
||||
assertEquals(asList("abc@def"), toTermString("@ABC@DEF@"));
|
||||
assertEquals(asList("abc", "def"), toTermString("&ABC&DEF&")); // XXX 3.x -> 8.x : abc&def -> abc def
|
||||
assertEquals(asList("abc", "def"), toTermString("@ABC@DEF@")); // XXX 3.x -> 8.x : abc@def -> abc def
|
||||
assertEquals(asList("abc'def"), toTermString("'ABC'DEF'"));
|
||||
|
||||
// trim and delimiter and number
|
||||
@@ -491,11 +511,15 @@ public class AnalyzerFactoryTestCase {
|
||||
assertEquals(asList("abc1", "def"), toTermString("*ABC1*DEF*"));
|
||||
assertEquals(asList("abc1", "def"), toTermString("?ABC1?DEF?"));
|
||||
assertEquals(asList("abc1", "def"), toTermString(":ABC1:DEF:"));
|
||||
assertEquals(asList("abc1,def"), toTermString(",ABC1,DEF,"));
|
||||
assertEquals(asList("abc1-def"), toTermString("-ABC1-DEF-"));
|
||||
assertEquals(asList("abc1/def"), toTermString("/ABC1/DEF/"));
|
||||
assertEquals(asList("abc1", "def"), toTermString(",ABC1,DEF,")); // XXX 3.x -> 8.x : abc1,def -> abc1 def
|
||||
assertEquals(asList("abc1", "def"), toTermString("-ABC1-DEF-")); // XXX 3.x -> 8.x : abc1-def -> abc1 def
|
||||
assertEquals(asList("abc1", "def"), toTermString("/ABC1/DEF/")); // XXX 3.x -> 8.x : abc1/def -> abc1 def
|
||||
/*
|
||||
* XXX 3.x -> 8.x : _abc1_def_ in UAX#29.
|
||||
* Since the effect is large, trim with Filter.
|
||||
*/
|
||||
assertEquals(asList("abc1_def"), toTermString("_ABC1_DEF_"));
|
||||
assertEquals(asList("abc1.def"), toTermString(".ABC1.DEF."));
|
||||
assertEquals(asList("abc1", "def"), toTermString(".ABC1.DEF.")); // XXX 3.x -> 8.x : abc1.def -> abc1 def
|
||||
assertEquals(asList("abc1", "def"), toTermString("&ABC1&DEF&"));
|
||||
assertEquals(asList("abc1", "def"), toTermString("@ABC1@DEF@"));
|
||||
assertEquals(asList("abc1", "def"), toTermString("'ABC1'DEF'"));
|
||||
@@ -517,14 +541,17 @@ public class AnalyzerFactoryTestCase {
|
||||
assertEquals("airsonic", terms.get(0));
|
||||
assertEquals("analysis", terms.get(1));
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* we ve -> we've
|
||||
*/
|
||||
query = "We’ve been here before.";
|
||||
terms = toTermString(query);
|
||||
assertEquals(5, terms.size());
|
||||
assertEquals("we", terms.get(0));
|
||||
assertEquals("ve", terms.get(1));
|
||||
assertEquals("been", terms.get(2));
|
||||
assertEquals("here", terms.get(3));
|
||||
assertEquals("before", terms.get(4));
|
||||
assertEquals(4, terms.size());
|
||||
assertEquals("we've", terms.get(0));
|
||||
assertEquals("been", terms.get(1));
|
||||
assertEquals("here", terms.get(2));
|
||||
assertEquals("before", terms.get(3));
|
||||
|
||||
query = "LʼHomme";
|
||||
terms = toTermString(query);
|
||||
@@ -591,6 +618,21 @@ public class AnalyzerFactoryTestCase {
|
||||
assertEquals("glasses", terms.get(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenre() {
|
||||
|
||||
/*
|
||||
* Confirming no conversion to singular.
|
||||
*/
|
||||
|
||||
String query = "{}";
|
||||
List<String> terms = toQueryTermString(FieldNames.GENRE, query);
|
||||
assertEquals(1, terms.size());
|
||||
assertEquals("{ }", terms.get(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private List<String> toTermString(String str) {
|
||||
return toTermString(null, str);
|
||||
}
|
||||
@@ -602,7 +644,7 @@ public class AnalyzerFactoryTestCase {
|
||||
new StringReader(str));
|
||||
stream.reset();
|
||||
while (stream.incrementToken()) {
|
||||
result.add(stream.getAttribute(TermAttribute.class).toString()
|
||||
result.add(stream.getAttribute(CharTermAttribute.class).toString()
|
||||
.replaceAll("^term\\=", ""));
|
||||
}
|
||||
stream.close();
|
||||
@@ -627,7 +669,7 @@ public class AnalyzerFactoryTestCase {
|
||||
new StringReader(str));
|
||||
stream.reset();
|
||||
while (stream.incrementToken()) {
|
||||
result.add(stream.getAttribute(TermAttribute.class).toString()
|
||||
result.add(stream.getAttribute(CharTermAttribute.class).toString()
|
||||
.replaceAll("^term\\=", ""));
|
||||
}
|
||||
stream.close();
|
||||
|
||||
+67
-43
@@ -8,9 +8,7 @@ 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;
|
||||
@@ -77,8 +75,34 @@ public class QueryFactoryTestCase {
|
||||
private static final List<MusicFolder> SINGLE_FOLDERS = Arrays.asList(MUSIC_FOLDER1);
|
||||
private static final List<MusicFolder> MULTI_FOLDERS = Arrays.asList(MUSIC_FOLDER1, MUSIC_FOLDER2);
|
||||
|
||||
|
||||
/*
|
||||
* XXX 3.x -> 8.x :
|
||||
* It does not change the basic functional requirements for the query.
|
||||
* However, some minor improvements are included.
|
||||
*
|
||||
* - Use 'Or' instead of 'SpanOr'.
|
||||
* This is suitable for 8.x document definition and query grammar.
|
||||
* A more rigorous comparison.
|
||||
*
|
||||
* - Removed comparison of input value and path from condition of search.
|
||||
* It causes a false search that the user can not imagine.
|
||||
* Originally unnecessary.
|
||||
*
|
||||
* - mediaType and genre changed to raw string key comparison.
|
||||
* Currently, these are "key" strings, both in the requirements and in the implementation.
|
||||
* The legacy "normalize" is dirty code that compensates for the incomplete analytics implementation
|
||||
* and is not necessary as long as proper key comparison can be done.
|
||||
*
|
||||
* => Treating these strictly as keys enables DB reference.
|
||||
* For example, can support multi-genre by creating a new genre field that implements another Tokenizer.
|
||||
*
|
||||
* - The method for comparing ranges of numbers has changed.
|
||||
* This is suitable for 8.x.
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void testSearchArtist() throws ParseException, IOException {
|
||||
public void testSearchArtist() throws IOException {
|
||||
SearchCriteria criteria = new SearchCriteria();
|
||||
criteria.setOffset(10);
|
||||
criteria.setCount(Integer.MAX_VALUE);
|
||||
@@ -86,16 +110,16 @@ public class QueryFactoryTestCase {
|
||||
|
||||
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.ARTIST);
|
||||
assertEquals("SearchArtist",
|
||||
"+((artist:abc* folder:abc*) (artist:def* folder:def*)) +spanOr([folder:" + PATH1 + "])",
|
||||
"+((artist:abc*) (artist:def*)) +(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());
|
||||
assertEquals("SearchArtist", "+((artist:abc*) (artist:def*)) +(folder:" + PATH1
|
||||
+ " folder:" + PATH2 + ")", query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchAlbum() throws ParseException, IOException {
|
||||
public void testSearchAlbum() throws IOException {
|
||||
SearchCriteria criteria = new SearchCriteria();
|
||||
criteria.setOffset(10);
|
||||
criteria.setCount(Integer.MAX_VALUE);
|
||||
@@ -103,19 +127,19 @@ public class QueryFactoryTestCase {
|
||||
|
||||
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
|
||||
+ "])",
|
||||
"+((album:abc* artist:abc*) (album:def* artist:def*)) +(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 + "])",
|
||||
"+((album:abc* artist:abc*) (album:def* artist:def*)) +(folder:" + PATH1
|
||||
+ " folder:" + PATH2 + ")",
|
||||
query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchSong() throws ParseException, IOException {
|
||||
public void testSearchSong() throws IOException {
|
||||
SearchCriteria criteria = new SearchCriteria();
|
||||
criteria.setOffset(10);
|
||||
criteria.setCount(Integer.MAX_VALUE);
|
||||
@@ -123,34 +147,34 @@ public class QueryFactoryTestCase {
|
||||
|
||||
Query query = queryFactory.search(criteria, SINGLE_FOLDERS, IndexType.SONG);
|
||||
assertEquals("SearchSong",
|
||||
"+((title:abc* artist:abc*) (title:def* artist:def*)) +spanOr([folder:" + PATH1 + "])",
|
||||
"+((title:abc* artist:abc*) (title:def* artist:def*)) +(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());
|
||||
assertEquals("SearchSong", "+((title:abc* artist:abc*) (title:def* artist:def*)) +(folder:" + PATH1
|
||||
+ " folder:" + PATH2 + ")", query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchArtistId3() throws ParseException, IOException {
|
||||
public void testSearchArtistId3() throws 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());
|
||||
assertEquals("SearchSong", "+((artist:abc*) (artist:def*)) +(folderId:"
|
||||
+ 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) + "])",
|
||||
"+((artist:abc*) (artist:def*)) +(folderId:" + FID1
|
||||
+ " folderId:" + FID2 + ")",
|
||||
query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchAlbumId3() throws ParseException, IOException {
|
||||
public void testSearchAlbumId3() throws IOException {
|
||||
SearchCriteria criteria = new SearchCriteria();
|
||||
criteria.setOffset(10);
|
||||
criteria.setCount(Integer.MAX_VALUE);
|
||||
@@ -158,74 +182,74 @@ public class QueryFactoryTestCase {
|
||||
|
||||
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) + "])",
|
||||
"SearchAlbumId3", "+((album:abc* artist:abc*) (album:def* artist:def*)) "
|
||||
+ "+(folderId:" + 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) + "])",
|
||||
"+((album:abc* artist:abc*) (album:def* artist:def*)) +(folderId:"
|
||||
+ FID1 + " folderId:"
|
||||
+ FID2 + ")",
|
||||
query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByNameArtist() throws ParseException {
|
||||
public void testSearchByNameArtist() throws IOException {
|
||||
Query query = queryFactory.searchByName(FieldNames.ARTIST, QUERY_ENG_ONLY);
|
||||
assertEquals("SearchByNameArtist", "artist:abc artist:def*", query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByNameAlbum() throws ParseException {
|
||||
public void testSearchByNameAlbum() throws IOException {
|
||||
Query query = queryFactory.searchByName(FieldNames.ALBUM, QUERY_ENG_ONLY);
|
||||
assertEquals("SearchByNameAlbum", "album:abc album:def*", query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByNameTitle() throws ParseException {
|
||||
public void testSearchByNameTitle() throws IOException {
|
||||
Query query = queryFactory.searchByName(FieldNames.TITLE, QUERY_ENG_ONLY);
|
||||
assertEquals("SearchByNameTitle", "title:abc title:def*", query.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRandomSongs() {
|
||||
public void testGetRandomSongs() throws IOException {
|
||||
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 + "])",
|
||||
"+mediaType:MUSIC +genre:Classic Rock +year:[1900 TO 2000] +(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
|
||||
+ "])",
|
||||
"+mediaType:MUSIC +genre:Classic Rock +year:[1900 TO 2000] +(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 + "])",
|
||||
"+mediaType:MUSIC +genre:Classic Rock +(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
|
||||
+ "])",
|
||||
"+mediaType:MUSIC +genre:Classic Rock +year:[1900 TO 2147483647] +(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
|
||||
+ "])",
|
||||
"+mediaType:MUSIC +genre:Classic Rock +year:[-2147483648 TO 2000] +(folder:" + PATH1 + " folder:" + PATH2
|
||||
+ ")",
|
||||
query.toString());
|
||||
}
|
||||
|
||||
@@ -233,23 +257,23 @@ public class QueryFactoryTestCase {
|
||||
public void testGetRandomAlbums() {
|
||||
Query query = queryFactory.getRandomAlbums(SINGLE_FOLDERS);
|
||||
assertEquals(ToStringBuilder.reflectionToString(SINGLE_FOLDERS),
|
||||
"spanOr([folder:" + PATH1 + "])", query.toString());
|
||||
"(folder:" + PATH1 + ")", query.toString());
|
||||
|
||||
query = queryFactory.getRandomAlbums(MULTI_FOLDERS);
|
||||
assertEquals(ToStringBuilder.reflectionToString(MULTI_FOLDERS),
|
||||
"spanOr([folder:" + PATH1 + ", folder:" + PATH2 + "])", query.toString());
|
||||
"(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());
|
||||
"(folderId:" + FID1 + ")", query.toString());
|
||||
|
||||
query = queryFactory.getRandomAlbumsId3(MULTI_FOLDERS);
|
||||
assertEquals(ToStringBuilder.reflectionToString(MULTI_FOLDERS),
|
||||
"spanOr([folderId:" + NumericUtils.intToPrefixCoded(FID1) + ", folderId:"
|
||||
+ NumericUtils.intToPrefixCoded(FID2) + "])",
|
||||
"(folderId:" + FID1 + " folderId:"
|
||||
+ FID2 + ")",
|
||||
query.toString());
|
||||
}
|
||||
|
||||
|
||||
+136
-58
@@ -52,103 +52,182 @@ public class SearchServiceSpecialGenreTestCase extends AbstractAirsonicHomeTest
|
||||
*
|
||||
* Legacy can not search all these genres.
|
||||
* (Strictly speaking, the genre field is not created at index creation.)
|
||||
*
|
||||
* // XXX 3.x -> 8.x : Do the process more strictly.
|
||||
*
|
||||
* - Values that can be cross-referenced with DB are stored in the index.
|
||||
* - Search is also possible with user's readable value (file tag value).
|
||||
* - However, there is an exception in parentheses.
|
||||
*/
|
||||
@Test
|
||||
public void testQueryEscapeRequires() {
|
||||
|
||||
List<MusicFolder> folders = getMusicFolders();
|
||||
|
||||
Function<String, RandomSearchCriteria> simpleStringCriteria = s ->
|
||||
new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
s, // genre,
|
||||
null, // fromYear
|
||||
null, // toYear
|
||||
folders // musicFolders
|
||||
);
|
||||
new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
s, // genre,
|
||||
null, // fromYear
|
||||
null, // toYear
|
||||
getMusicFolders() // musicFolders
|
||||
);
|
||||
|
||||
List<MediaFile> songs = searchService.getRandomSongs(simpleStringCriteria.apply("+"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("+", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 1", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("-"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("-", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 2", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("&&"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("+"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("&&", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 3", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("||"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("||", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 4", songs.get(0).getTitle());
|
||||
|
||||
/*
|
||||
* // XXX 3.x -> 8.x : Brackets ()
|
||||
*
|
||||
* Lucene can handle these.
|
||||
* However, brackets are specially parsed before the index creation process.
|
||||
*
|
||||
* This string is never stored in the index.
|
||||
* This is the only exception.
|
||||
*/
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply(" ("));// space & bracket
|
||||
Assert.assertEquals(0, songs.size());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply(")"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
|
||||
/*
|
||||
* // XXX 3.x -> 8.x : Brackets {}[]
|
||||
*
|
||||
* Lucene can handle these.
|
||||
* However, brackets are specially parsed before the index creation process.
|
||||
*
|
||||
* This can be done with a filter that performs the reverse process
|
||||
* on the input values when searching.
|
||||
* As a result, the values stored in the file can be retrieved by search.
|
||||
*
|
||||
* @see AnalyzerFactory
|
||||
*
|
||||
* >>>>>
|
||||
*/
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("{}"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
/*
|
||||
* This is the result of the tag parser and domain value.
|
||||
* It is different from the tag value in file.
|
||||
*/
|
||||
Assert.assertEquals("{ }", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 7", songs.get(0).getTitle());
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("{ }"));
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("Query Escape Requires 7", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("[]"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
/*
|
||||
* This is the result of the tag parser and domain value.
|
||||
* It is different from the tag value in file.
|
||||
*/
|
||||
Assert.assertEquals("[ ]", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 8", songs.get(0).getTitle());
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("[ ]"));
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("Query Escape Requires 8", songs.get(0).getTitle());
|
||||
// <<<<<
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("^"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("^", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 9", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("\""));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("\"", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 10", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("~"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("~", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 11", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("*"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("*", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 12", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("?"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("?", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 13", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply(":"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals(":", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 14", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("\\"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("\\", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 15", songs.get(0).getTitle());
|
||||
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("/"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
Assert.assertEquals(1, songs.size());// XXX 3.x -> 8.x : Searchable
|
||||
Assert.assertEquals("/", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Query Escape Requires 16", songs.get(0).getTitle());
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Jaudiotagger applies special treatment to bracket (FILE17).
|
||||
*
|
||||
* XXX 3.x -> 8.x : Specification of genre search became more natural.
|
||||
*/
|
||||
@Test
|
||||
public void testBrackets() {
|
||||
|
||||
List<MusicFolder> folders = getMusicFolders();
|
||||
|
||||
RandomSearchCriteria criteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
"-(GENRE)-", // genre,
|
||||
Function<String, RandomSearchCriteria> simpleStringCriteria = s ->
|
||||
new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
s, // genre,
|
||||
null, // fromYear
|
||||
null, // toYear
|
||||
folders // musicFolders
|
||||
getMusicFolders() // musicFolders
|
||||
);
|
||||
|
||||
List<MediaFile> songs = searchService.getRandomSongs(criteria);
|
||||
Assert.assertEquals(0, songs.size());
|
||||
// -(GENRE)- is registered as genre of FILE17.
|
||||
|
||||
criteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
" genre", // genre,
|
||||
null, // fromYear
|
||||
null, // toYear
|
||||
folders // musicFolders
|
||||
);
|
||||
|
||||
songs = searchService.getRandomSongs(criteria);
|
||||
/*
|
||||
* Search by genre string registered in file.
|
||||
*
|
||||
* The value stored in the index is different from legacy.
|
||||
* Domain value is kept as it is.
|
||||
*/
|
||||
List<MediaFile> songs = searchService.getRandomSongs(simpleStringCriteria.apply("-(GENRE)-"));
|
||||
Assert.assertEquals(1, songs.size());
|
||||
Assert.assertEquals("Consistency with Tag Parser 1", songs.get(0).getTitle());
|
||||
Assert.assertEquals("-GENRE -", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Consistency with Tag Parser 1", songs.get(0).getTitle());
|
||||
|
||||
/*
|
||||
* Search by Domain value.
|
||||
*/
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("-GENRE -"));
|
||||
Assert.assertEquals(1, songs.size());
|
||||
Assert.assertEquals("-GENRE -", songs.get(0).getGenre());
|
||||
Assert.assertEquals("Consistency with Tag Parser 1", songs.get(0).getTitle());
|
||||
|
||||
/*
|
||||
* Legacy genre search
|
||||
*/
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply(" genre"));
|
||||
// Strong unique parsing rules have been removed.
|
||||
Assert.assertEquals(0, songs.size());
|
||||
|
||||
}
|
||||
|
||||
@@ -188,29 +267,28 @@ public class SearchServiceSpecialGenreTestCase extends AbstractAirsonicHomeTest
|
||||
@Test
|
||||
public void testOthers() {
|
||||
|
||||
List<MusicFolder> folders = getMusicFolders();
|
||||
|
||||
RandomSearchCriteria criteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
"{'“『【【】】[︴○◎@ $〒→+]FULL-WIDTHCæsar's", // genre,
|
||||
Function<String, RandomSearchCriteria> simpleStringCriteria = s ->
|
||||
new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
s, // genre,
|
||||
null, // fromYear
|
||||
null, // toYear
|
||||
folders // musicFolders
|
||||
getMusicFolders() // musicFolders
|
||||
);
|
||||
|
||||
List<MediaFile> songs = searchService.getRandomSongs(criteria);
|
||||
Assert.assertEquals(0, songs.size());
|
||||
|
||||
criteria = new RandomSearchCriteria(Integer.MAX_VALUE, // count
|
||||
"widthcaesar", // genre,
|
||||
null, // fromYear
|
||||
null, // toYear
|
||||
folders // musicFolders
|
||||
);
|
||||
|
||||
songs = searchService.getRandomSongs(criteria);
|
||||
// XXX 3.x -> 8.x : Do the process more strictly.
|
||||
List<MediaFile> songs = searchService
|
||||
.getRandomSongs(simpleStringCriteria.apply("{'“『【【】】[︴○◎@ $〒→+]FULL-WIDTHCæsar's"));
|
||||
Assert.assertEquals(1, songs.size());
|
||||
Assert.assertEquals(1, songs.size());
|
||||
Assert.assertEquals("Other special strings 1", songs.get(0).getTitle());
|
||||
Assert.assertEquals("{'“『【【】】[︴○◎@ $〒→+]FULL-WIDTHCæsar's", songs.get(0).getGenre());
|
||||
|
||||
/*
|
||||
* Legacy kept "widthcaesar" using their own rules.
|
||||
* The previous rule has been discarded.
|
||||
*/
|
||||
songs = searchService.getRandomSongs(simpleStringCriteria.apply("widthcaesar"));
|
||||
Assert.assertEquals(0, songs.size());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -56,11 +56,23 @@ public class SearchServiceStartWithStopwardsTestCase extends AbstractAirsonicHom
|
||||
|
||||
criteria.setQuery("will");
|
||||
SearchResult result = searchService.search(criteria, folders, IndexType.ARTIST_ID3);
|
||||
Assert.assertEquals("Williams hit by \"will\" ", 1, result.getTotalHits());
|
||||
// XXX 3.x -> 8.x : The filter is properly applied to the input(Stopward)
|
||||
Assert.assertEquals("Williams hit by \"will\" ", 0, result.getTotalHits());
|
||||
|
||||
criteria.setQuery("the");
|
||||
result = searchService.search(criteria, folders, IndexType.SONG);
|
||||
Assert.assertEquals("Theater hit by \"the\" ", 1, result.getTotalHits());
|
||||
// XXX 3.x -> 8.x : The filter is properly applied to the input(Stopward)
|
||||
Assert.assertEquals("Theater hit by \"the\" ", 0, result.getTotalHits());
|
||||
|
||||
criteria.setQuery("willi");
|
||||
result = searchService.search(criteria, folders, IndexType.ARTIST_ID3);
|
||||
// XXX 3.x -> 8.x : Normal forward matching
|
||||
Assert.assertEquals("Williams hit by \"Williams\" ", 1, result.getTotalHits());
|
||||
|
||||
criteria.setQuery("thea");
|
||||
result = searchService.search(criteria, folders, IndexType.SONG);
|
||||
// XXX 3.x -> 8.x : Normal forward matching
|
||||
Assert.assertEquals("Theater hit by \"thea\" ", 1, result.getTotalHits());
|
||||
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -164,7 +164,12 @@ public class SearchServiceTestCase extends AbstractAirsonicHomeTest {
|
||||
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,
|
||||
|
||||
/*
|
||||
* // XXX 3.x -> 8.x :
|
||||
* Hit 'Nash*' as the slash becomes a delimiter.
|
||||
*/
|
||||
Assert.assertEquals("(21) Specify '" + query + "' as the name, and get an artist.", 1,
|
||||
artistResult.getItems().size());
|
||||
|
||||
// *** testGetRandomSongs() ***
|
||||
|
||||
Reference in New Issue
Block a user