blob: 4a5612c39516ea358ec65ae0deddedfdb90aadf9 [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
r"""Core Bloodhound Search components."""
from trac.core import *
from trac.config import ExtensionOption
ASC = "asc"
DESC = "desc"
SCORE = "score"
class IndexFields(object):
TYPE = "type"
ID = "id"
TIME = 'time'
AUTHOR = 'author'
CONTENT = 'content'
class QueryResult(object):
def __init__(self):
self.hits = 0
self.page_count = 0
self.page_number = 0
self.offset = 0
self.docs = []
self.facets = None
self.debug = {}
class ISearchBackend(Interface):
"""Extension point interface for search backend systems.
"""
# def add_doc(self, doc, **kwargs):
def add_doc(doc, **kwargs):
"""
Called when new document instance must be added
"""
def delete_doc(type, id, **kwargs):
"""
Delete document from index
"""
def optimize():
"""
Optimize index if needed
"""
def commit(optimize, **kwargs):
"""
Commit changes
"""
def cancel(**kwargs):
"""
Cancel changes if possible
"""
def recreate_index():
"""
Create a new index, if index exists, it will be deleted
"""
def open_or_create_index_if_missing():
"""
Open existing index, if index does not exist, create new one
"""
def query(query, sort = None, fields = None, boost = None, filter = None,
facets = None, pagenum = 1, pagelen = 20):
"""
Perform query implementation
:param query:
:param sort:
:param fields:
:param boost:
:param filter:
:param facets:
:param pagenum:
:param pagelen:
:return: ResultsPage
"""
def start_operation(self):
"""Used to get arguments for batch operation withing single commit"""
class IIndexParticipant(Interface):
"""Extension point interface for components that should be searched.
"""
def get_entries_for_index():
"""List entities for index creation"""
class ISearchParticipant(Interface):
"""Extension point interface for components that should be searched.
"""
def format_search_results(contents):
"""Called to see if the module wants to format the search results."""
def get_search_filters(req):
"""Called when we want to build the list of components with search.
Passes the request object to do permission checking."""
def get_title():
"""Return resource title"""
class IQueryParser(Interface):
"""Extension point for Bloodhound Search query parser.
"""
def parse(query_string, req = None):
"""Parse query from string"""
class BloodhoundSearchApi(Component):
"""Implements core indexing functionality, provides methods for
searching, adding and deleting documents from index.
"""
backend = ExtensionOption('bhsearch', 'search_backend',
ISearchBackend, 'WhooshBackend',
'Name of the component implementing Bloodhound Search backend \
interface: ISearchBackend.')
parser = ExtensionOption('bhsearch', 'query_parser',
IQueryParser, 'DefaultQueryParser',
'Name of the component implementing Bloodhound Search query \
parser.')
index_participants = ExtensionPoint(IIndexParticipant)
def query(self, query, sort = None, fields = None,
boost = None, filter = None,
facets = None, pagenum = 1, pagelen = 20):
"""Return query result from an underlying search backend.
Arguments:
:param query: query string e.g. “bla status:closed” or a parsed
representation of the query.
:param sort: optional sorting
:param boost: optional list of fields with boost values e.g.
{“id”: 1000, “subject” :100, “description”:10}.
:param filter: optional list of terms. Usually can be cached by
underlying search framework. For example {“type”: “wiki”}
:param facets: optional list of facet terms, can be field or expression
:param page: paging support
:param pagelen: paging support
:return: result QueryResult
"""
self.env.log.debug("Receive query request: %s", locals())
parsed_query = self.parser.parse(query)
# TODO: add query parsers and meta keywords post-parsing
# TODO: apply security filters
query_result = self.backend.query(
query = parsed_query,
sort = sort,
fields = fields,
filter = filter,
facets = facets,
pagenum = pagenum,
pagelen = pagelen,
)
return query_result
def rebuild_index(self):
"""Rebuild underlying index"""
self.log.info('Rebuilding the search index.')
self.backend.recreate_index()
operation_data = self.backend.start_operation()
try:
for participant in self.index_participants:
docs = participant.get_entries_for_index()
for doc in docs:
self.backend.add_doc(doc, **operation_data)
self.backend.commit(True, **operation_data)
except:
self.backend.cancel(**operation_data)
raise
def change_doc_id(self, doc, old_id):
operation_data = self.backend.start_operation()
try:
self.backend.delete_doc(
doc[IndexFields.TYPE],
old_id,
**operation_data)
self.backend.add_doc(doc, **operation_data)
self.backend.commit(False, **operation_data)
except:
self.backend.cancel(**operation_data)
raise
def optimize(self):
"""Optimize underlying index"""
self.backend.optimize()
def add_doc(self, doc):
"""Add a document to underlying search backend.
The doc must be dictionary with obligatory "type" field
"""
operation_data = self.backend.start_operation()
try:
self.backend.add_doc(doc, **operation_data)
self.backend.commit(False, **operation_data)
except:
self.backend.cancel(**operation_data)
raise
def delete_doc(self, type, id):
"""Add a document from underlying search backend.
The doc must be dictionary with obligatory "type" field
"""
operation_data = self.backend.start_operation()
try:
self.backend.delete_doc(type, id, **operation_data)
self.backend.commit(False, **operation_data)
except:
self.backend.cancel(**operation_data)
raise