| #!/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 |
| |
| |
| |