Merge pull request #334 from martenson/ts-search-expansion

TS search refactoring and bugfix
This commit is contained in:
John Chilton
2015-06-15 09:46:21 -04:00
5 changed files with 137 additions and 157 deletions
@@ -16,13 +16,16 @@ from galaxy.exceptions import InsufficientPermissionsException
from galaxy.exceptions import ActionInputError
from galaxy.exceptions import ObjectNotFound
from galaxy.exceptions import MalformedId
from galaxy.exceptions import ConfigDoesNotAllowException
from galaxy.datatypes import checkers
from galaxy.model.orm import and_
from galaxy.web import _future_expose_api as expose_api
from galaxy.web import _future_expose_api_anonymous_and_sessionless as expose_api_anonymous_and_sessionless
from galaxy.web import _future_expose_api_raw_anonymous_and_sessionless as expose_api_raw_anonymous_and_sessionless
from galaxy.web.base.controller import BaseAPIController
from galaxy.web.base.controller import HTTPBadRequest
from galaxy.web.framework.helpers import time_ago
from galaxy.webapps.tool_shed.search.repo_search import RepoSearch
from tool_shed.capsule import capsule_manager
from tool_shed.metadata import repository_metadata_manager
@@ -316,20 +319,54 @@ class RepositoriesController( BaseAPIController ):
return_dict[ key ] = val
return return_dict
@web.expose_api_anonymous
@expose_api_raw_anonymous_and_sessionless
def index( self, trans, deleted=False, owner=None, name=None, **kwd ):
"""
GET /api/repositories
Displays a collection of repositories with optional criteria.
:param deleted: True/False, displays repositories that are or are not set to deleted.
:param owner: the owner's public username.
:param name: the repository name.
:param q: (optional)if present search on the given query will be performed
:type q: str
Displays a collection (list) of repositories.
:param page: (optional)requested page of the search
:type page: int
:param jsonp: (optional)flag whether to use jsonp format response, defaults to False
:type jsonp: bool
:param callback: (optional)name of the function to wrap callback in
used only when jsonp is true, defaults to 'callback'
:type callback: str
:param deleted: (optional)displays repositories that are or are not set to deleted.
:type deleted: bool
:param owner: (optional)the owner's public username.
:type owner: str
:param name: (optional)the repository name.
:type name: str
:returns dict: object containing list of results
Examples:
GET http://localhost:9009/api/repositories
GET http://localhost:9009/api/repositories?q=fastq
"""
# Example URL: http://localhost:9009/api/repositories
repository_dicts = []
deleted = util.asbool( deleted )
q = kwd.get( 'q', '' )
if q:
page = kwd.get( 'page', 1 )
return_jsonp = util.asbool( kwd.get( 'jsonp', False ) )
callback = kwd.get( 'callback', 'callback' )
search_results = self._search( trans, q, page )
if return_jsonp:
response = str( '%s(%s);' % ( callback, json.dumps( search_results ) ) )
else:
response = json.dumps( search_results )
return response
clause_list = [ and_( trans.app.model.Repository.table.c.deprecated == False,
trans.app.model.Repository.table.c.deleted == deleted ) ]
if owner is not None:
@@ -345,7 +382,28 @@ class RepositoriesController( BaseAPIController ):
repository_dict[ 'category_ids' ] = \
[ trans.security.encode_id( x.category.id ) for x in repository.categories ]
repository_dicts.append( repository_dict )
return repository_dicts
return json.dumps( repository_dicts )
def _search( self, trans, q, page=1 ):
"""
Perform the search over TS repositories.
Note that search works over the Whoosh index which you have
to pre-create with scripts/tool_shed/build_ts_whoosh_index.sh manually.
Also TS config option toolshed_search_on has to be True and
whoosh_index_dir has to be specified.
"""
if not self.app.config.toolshed_search_on:
raise ConfigDoesNotAllowException( 'Searching the TS through the API is turned off for this instance.' )
if not self.app.config.whoosh_index_dir:
raise ConfigDoesNotAllowException( 'There is no directory for the search index specified. Please contact the administrator.' )
search_term = q.strip()
if len( search_term ) < 3:
raise RequestParameterInvalidException( 'The search term has to be at least 3 characters long.' )
repo_search = RepoSearch()
results = repo_search.search( trans, search_term, page )
results[ 'hostname' ] = web.url_for( '/', qualified = True )
return results
@web.expose_api
def remove_repository_registry_entry( self, trans, payload, **kwd ):
@@ -1,60 +0,0 @@
"""API for searching the toolshed repositories"""
from galaxy import exceptions
from galaxy import eggs
from galaxy import util
from galaxy.web import _future_expose_api_raw_anonymous_and_sessionless as expose_api_raw_anonymous_and_sessionless
from galaxy.web.base.controller import BaseAPIController
from galaxy.webapps.tool_shed.search.repo_search import RepoSearch
from galaxy.web import url_for
import json
import logging
log = logging.getLogger( __name__ )
class SearchController ( BaseAPIController ):
@expose_api_raw_anonymous_and_sessionless
def search( self, trans, search_term, **kwd ):
"""
Perform a search over the Whoosh index.
The index has to be pre-created with build_ts_whoosh_index.sh.
TS config option toolshed_search_on has to be turned on and
whoosh_index_dir has to be specified.
:param search_term: (required)the term to search
:type search_term: str
:param page: (optional)requested page of the search
:type page: int
:param jsonp: (optional)flag whether to use jsonp format response, defaults to False
:type jsonp: bool
:param callback: (optional)name of the function to wrap callback in
used only when jsonp is true, defaults to 'callback'
:type callback: str
:returns dict: object containing list of results and a hostname
"""
if not self.app.config.toolshed_search_on:
raise exceptions.ConfigDoesNotAllowException( 'Searching the TS through the API is turned off for this instance.' )
if not self.app.config.whoosh_index_dir:
raise exceptions.ConfigDoesNotAllowException( 'There is no directory for the search index specified. Please ontact the administrator.' )
search_term = search_term.strip()
if len( search_term ) < 3:
raise exceptions.RequestParameterInvalidException( 'The search term has to be at least 3 characters long.' )
page = kwd.get( 'page', 1 )
return_jsonp = util.asbool( kwd.get( 'jsonp', False ) )
callback = kwd.get( 'callback', 'callback' )
repo_search = RepoSearch()
results = repo_search.search( trans, search_term, page )
results[ 'hostname' ] = url_for( '/', qualified = True )
if return_jsonp:
response = str( '%s(%s);' % ( callback, json.dumps( results ) ) )
else:
response = json.dumps( results )
return response
-5
View File
@@ -101,11 +101,6 @@ def app_factory( global_conf, **kwargs ):
controller='groups',
action='show',
conditions=dict( method=[ "GET" ] ) )
webapp.mapper.connect( 'repo_search',
'/api/search/',
controller='search',
action='search',
conditions=dict( method=[ "GET" ] ) )
webapp.mapper.resource( 'category',
'categories',
controller='categories',
@@ -6,36 +6,26 @@ from galaxy.webapps.tool_shed import model
import logging
log = logging.getLogger( __name__ )
# Whoosh is compatible with Python 2.5+
# Try to import Whoosh and set flag to indicate whether
# the tool search is ready.
try:
eggs.require( "Whoosh" )
import whoosh.index
from whoosh import scoring
from whoosh.fields import Schema, STORED, ID, KEYWORD, TEXT, STORED
from whoosh.scoring import BM25F
from whoosh.qparser import MultifieldParser
from whoosh.index import Index
search_ready = True
eggs.require( "Whoosh" )
import whoosh.index
from whoosh import scoring
from whoosh.fields import Schema, STORED, ID, KEYWORD, TEXT, STORED
from whoosh.scoring import BM25F
from whoosh.qparser import MultifieldParser
from whoosh.index import Index
schema = Schema(
id = STORED,
name = TEXT( field_boost = 1.7, stored = True ),
description = TEXT( field_boost = 1.5, stored = True ),
long_description = TEXT( stored = True ),
homepage_url = TEXT( stored = True ),
remote_repository_url = TEXT( stored = True ),
repo_owner_username = TEXT( stored = True ),
times_downloaded = STORED,
approved = STORED,
last_updated = STORED,
full_last_updated = STORED )
except ImportError, e:
log.error( 'There was an error while importing the Whoosh library.' )
search_ready = False
schema = None
schema = Schema(
id = STORED,
name = TEXT( field_boost = 1.7, stored = True ),
description = TEXT( field_boost = 1.5, stored = True ),
long_description = TEXT( stored = True ),
homepage_url = TEXT( stored = True ),
remote_repository_url = TEXT( stored = True ),
repo_owner_username = TEXT( stored = True ),
times_downloaded = STORED,
approved = STORED,
last_updated = STORED,
full_last_updated = STORED )
class RepoWeighting( scoring.BM25F ):
@@ -82,60 +72,57 @@ class RepoSearch( object ):
:returns results: dictionary containing number of hits, hits themselves and matched terms for each
"""
if search_ready:
whoosh_index_dir = trans.app.config.whoosh_index_dir
index_exists = whoosh.index.exists_in( whoosh_index_dir )
if index_exists:
index = whoosh.index.open_dir( whoosh_index_dir )
try:
# Some literature about BM25F:
# http://trec.nist.gov/pubs/trec13/papers/microsoft-cambridge.web.hard.pdf
# http://en.wikipedia.org/wiki/Okapi_BM25
# __Basically__ the higher number the bigger weight.
repo_weighting = RepoWeighting( field_B = { 'name_B' : 0.9,
'description_B' : 0.6,
'long_description_B' : 0.5,
'homepage_url_B' : 0.3,
'remote_repository_url_B' : 0.2,
'repo_owner_username' : 0.3 } )
whoosh_index_dir = trans.app.config.whoosh_index_dir
index_exists = whoosh.index.exists_in( whoosh_index_dir )
if index_exists:
index = whoosh.index.open_dir( whoosh_index_dir )
try:
# Some literature about BM25F:
# http://trec.nist.gov/pubs/trec13/papers/microsoft-cambridge.web.hard.pdf
# http://en.wikipedia.org/wiki/Okapi_BM25
# __Basically__ the higher number the bigger weight.
repo_weighting = RepoWeighting( field_B = { 'name_B' : 0.9,
'description_B' : 0.6,
'long_description_B' : 0.5,
'homepage_url_B' : 0.3,
'remote_repository_url_B' : 0.2,
'repo_owner_username' : 0.3 } )
searcher = index.searcher( weighting = repo_weighting )
searcher = index.searcher( weighting = repo_weighting )
parser = MultifieldParser( [
'name',
'description',
'long_description',
'homepage_url',
'remote_repository_url',
'repo_owner_username' ], schema = schema )
parser = MultifieldParser( [
'name',
'description',
'long_description',
'homepage_url',
'remote_repository_url',
'repo_owner_username' ], schema = schema )
user_query = parser.parse( '*' + search_term + '*' )
hits = searcher.search_page( user_query, page, pagelen = 10, terms = True )
user_query = parser.parse( '*' + search_term + '*' )
hits = searcher.search_page( user_query, page, pagelen = 10, terms = True )
log.debug( 'searching for: #' + str( search_term ) )
log.debug( 'total hits: ' + str( len( hits ) ) )
log.debug( 'scored hits: ' + str( hits.scored_length() ) )
results = {}
results[ 'total_results'] = str( len( hits ) )
results[ 'hits' ] = []
for hit in hits:
hit_dict = {}
hit_dict[ 'id' ] = trans.security.encode_id( hit.get( 'id' ) )
hit_dict[ 'repo_owner_username' ] = hit.get( 'repo_owner_username' )
hit_dict[ 'name' ] = hit.get( 'name' )
hit_dict[ 'long_description' ] = hit.get( 'long_description' )
hit_dict[ 'remote_repository_url' ] = hit.get( 'remote_repository_url' )
hit_dict[ 'homepage_url' ] = hit.get( 'homepage_url' )
hit_dict[ 'description' ] = hit.get( 'description' )
hit_dict[ 'last_updated' ] = hit.get( 'last_updated' )
hit_dict[ 'full_last_updated' ] = hit.get( 'full_last_updated' )
hit_dict[ 'approved' ] = hit.get( 'approved' )
hit_dict[ 'times_downloaded' ] = hit.get( 'times_downloaded' )
results[ 'hits' ].append( {'repository': hit_dict, 'matched_terms': hit.matched_terms(), 'score': hit.score } )
return results
finally:
searcher.close()
else:
raise exceptions.InternalServerError( 'The search index file is missing.' )
log.debug( 'searching for: #' + str( search_term ) )
log.debug( 'total hits: ' + str( len( hits ) ) )
log.debug( 'scored hits: ' + str( hits.scored_length() ) )
results = {}
results[ 'total_results'] = str( len( hits ) )
results[ 'hits' ] = []
for hit in hits:
hit_dict = {}
hit_dict[ 'id' ] = trans.security.encode_id( hit.get( 'id' ) )
hit_dict[ 'repo_owner_username' ] = hit.get( 'repo_owner_username' )
hit_dict[ 'name' ] = hit.get( 'name' )
hit_dict[ 'long_description' ] = hit.get( 'long_description' )
hit_dict[ 'remote_repository_url' ] = hit.get( 'remote_repository_url' )
hit_dict[ 'homepage_url' ] = hit.get( 'homepage_url' )
hit_dict[ 'description' ] = hit.get( 'description' )
hit_dict[ 'last_updated' ] = hit.get( 'last_updated' )
hit_dict[ 'full_last_updated' ] = hit.get( 'full_last_updated' )
hit_dict[ 'approved' ] = hit.get( 'approved' )
hit_dict[ 'times_downloaded' ] = hit.get( 'times_downloaded' )
results[ 'hits' ].append( {'repository': hit_dict, 'matched_terms': hit.matched_terms(), 'score': hit.score } )
return results
finally:
searcher.close()
else:
raise exceptions.InternalServerError( 'Could not initialize search.' )
raise exceptions.InternalServerError( 'The search index file is missing.' )
+4 -4
View File
@@ -2,13 +2,14 @@
Build indexes for searching the TS.
Run this script from the Tool Shed folder, example:
$ python scripts/tool_shed/build_ts_whoosh_index_test.py -c config/tool_shed.ini -r database/community_files
$ python scripts/tool_shed/build_ts_whoosh_index.py -c config/tool_shed.ini
Make sure you adjusted your config to:
* turn on searching via toolshed_search_on
* specify whoosh_index_dir where the indexes will be placed
Also make sure that GALAXY_EGGS_PATH variable is properly set.
Also make sure that GALAXY_EGGS_PATH variable is properly set
in case you are using non-default location for Galaxy.
"""
import sys
import os
@@ -205,10 +206,9 @@ def get_sa_session_and_needed_config_settings( path_to_tool_shed_config ):
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-c", "--config", dest="path_to_tool_shed_config", default="config/tool_shed.ini", help="specify tool_shed.ini location")
parser.add_option("-r", "--repos", dest="path_to_repositories", default="database/community_files", help="specify path to folder with repositories")
(options, args) = parser.parse_args()
path_to_tool_shed_config = options.path_to_tool_shed_config
path_to_repositories = options.path_to_repositories
sa_session, config_settings = get_sa_session_and_needed_config_settings( path_to_tool_shed_config )
whoosh_index_dir = config_settings.get( 'whoosh_index_dir', None )
path_to_repositories = config_settings.get( 'file_path', 'database/community_files' )
build_index( sa_session, whoosh_index_dir, path_to_repositories )