/* 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 . Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service.search; import org.airsonic.player.domain.*; import org.airsonic.player.service.SearchService; import org.apache.lucene.document.Document; import org.apache.lucene.search.*; import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.*; import java.util.function.BiConsumer; import java.util.stream.Collectors; import static org.airsonic.player.service.search.IndexType.*; import static org.springframework.util.ObjectUtils.isEmpty; @Service public class SearchServiceImpl implements SearchService { private static final Logger LOG = LoggerFactory.getLogger(SearchServiceImpl.class); @Autowired private QueryFactory queryFactory; @Autowired private IndexManager indexManager; @Autowired private SearchServiceUtilities util; @Override public SearchResult search(SearchCriteria criteria, List musicFolders, IndexType indexType) { SearchResult result = new SearchResult(); int offset = criteria.getOffset(); int count = criteria.getCount(); result.setOffset(offset); if (count <= 0) return result; IndexSearcher searcher = indexManager.getSearcher(indexType); if (isEmpty(searcher)) { return result; } try { Query query = queryFactory.search(criteria, musicFolders, indexType); 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 e) { LOG.error("Failed to execute Lucene search.", e); } finally { indexManager.release(indexType, searcher); } return result; } /** * Common processing of random method. * * @param count Number of albums to return. * @param id2ListCallBack Callback to get D from id and store it in List */ private final List createRandomDocsList( int count, IndexSearcher searcher, Query query, BiConsumer, Integer> id2ListCallBack) throws IOException { List docs = Arrays .stream(searcher.search(query, Integer.MAX_VALUE).scoreDocs) .map(sd -> sd.doc) .collect(Collectors.toList()); List result = new ArrayList<>(); while (!docs.isEmpty() && result.size() < count) { int randomPos = util.nextInt.apply(docs.size()); Document document = searcher.doc(docs.get(randomPos)); id2ListCallBack.accept(result, util.getId.apply(document)); docs.remove(randomPos); } return result; } @Override public List getRandomSongs(RandomSearchCriteria criteria) { IndexSearcher searcher = indexManager.getSearcher(SONG); if (isEmpty(searcher)) { // At first start return Collections.emptyList(); } try { Query query = queryFactory.getRandomSongs(criteria); return createRandomDocsList(criteria.getCount(), searcher, query, (dist, id) -> util.addIgnoreNull(dist, SONG, id)); } catch (IOException e) { LOG.error("Failed to search or random songs.", e); } finally { indexManager.release(IndexType.SONG, searcher); } return Collections.emptyList(); } @Override public List getRandomAlbums(int count, List musicFolders) { IndexSearcher searcher = indexManager.getSearcher(IndexType.ALBUM); if (isEmpty(searcher)) { return Collections.emptyList(); } Query query = queryFactory.getRandomAlbums(musicFolders); try { 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 { indexManager.release(IndexType.ALBUM, searcher); } return Collections.emptyList(); } @Override public List getRandomAlbumsId3(int count, List musicFolders) { IndexSearcher searcher = indexManager.getSearcher(IndexType.ALBUM_ID3); if (isEmpty(searcher)) { return Collections.emptyList(); } Query query = queryFactory.getRandomAlbumsId3(musicFolders); try { 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 { indexManager.release(IndexType.ALBUM_ID3, searcher); } return Collections.emptyList(); } @Override public ParamSearchResult searchByName(String name, int offset, int count, List folderList, Class assignableClass) { // we only support album, artist, and song for now @Nullable IndexType indexType = util.getIndexType.apply(assignableClass); @Nullable String fieldName = util.getFieldName.apply(assignableClass); ParamSearchResult result = new ParamSearchResult(); result.setOffset(offset); if (isEmpty(indexType) || isEmpty(fieldName) || count <= 0) { return result; } IndexSearcher searcher = indexManager.getSearcher(indexType); if (isEmpty(searcher)) { return result; } try { Query query = queryFactory.searchByName(fieldName, name); SortField[] sortFields = Arrays .stream(indexType.getFields()) .map(n -> new SortField(n, SortField.Type.STRING)) .toArray(i -> new SortField[i]); Sort sort = new Sort(sortFields); 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 e) { LOG.error("Failed to execute Lucene search.", e); } finally { indexManager.release(indexType, searcher); } return result; } }