You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@bloodhound.apache.org by Antonia Horincar <an...@gmail.com> on 2014/08/13 18:15:18 UTC

Re: svn commit: r1617747 - in /bloodhound/branches/bep_0014_solr: bloodhound_search/bhsearch/ bloodhound_search/bhsearch/templates/ bloodhound_solr/ bloodhound_solr/bhsolr/ bloodhound_solr/bhsolr/schemadoc/ bloodhound_solr/bhsolr/search_resources/ bloodhou...

“More like this” results are now shown in the interface. I wasn't sure how they should be displayed, so I used a Bootstrap collapsible component for each original result, so when the user clicks on the collapsible button, the list of results is shown. Also, even though I tried to implement this feature without modifying other Bloodhound files (that are not part of the Solr plugin), I didn’t manage to. I had to make some changes in the bhsearch.api, bhsearch.web_ui and bhtheme.theme modules in order to keep track of the “more like this” results. Also, I probably didn’t understand very well how the templating system works in Bloodhound at the moment, because I couldn’t get the desired results without adding a new template in bhtheme. My initial plan was to create a template in the Solr plugin (by implementing ITemplateProvider), and include it in the bhsearch.html template already existing in bhsearch. But it didn’t work, the template didn’t seem to be recognised as a Trac template (I kept getting the “Template not found” error.

I also formatted the code to PEP-8 standard (and am planning on adding comments next). Also, I'm working on the installation guide and on developing unit tests for the plugin.  

On 13 August 2014 at 19:08:30, ahorincar@apache.org (ahorincar@apache.org) wrote:

Author: ahorincar  
Date: Wed Aug 13 16:08:04 2014  
New Revision: 1617747  

URL: http://svn.apache.org/r1617747  
Log:  
Finished More Like This feature, formatted code, refactored code  

Added:  
bloodhound/branches/bep_0014_solr/bloodhound_solr/README  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/backend.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/search_resources/  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/search_resources/__init__.py  
bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html  
Removed:  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/backend.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schemadoc/  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/web_ui.py  
Modified:  
bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/api.py  
bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/templates/bhsearch.html  
bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/web_ui.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/__init__.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/__init__.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/changeset_search.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/milestone_search.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/ticket_search.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/wiki_search.py  
bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py  
bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/theme.py  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/api.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/api.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/api.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/api.py Wed Aug 13 16:08:04 2014  
@@ -319,13 +319,20 @@ class BloodhoundSearchApi(Component):  
for query_processor in self.query_processors:  
query_processor.query_pre_process(query_parameters, context)  

- query_result = self.backend.query(**query_parameters)  
+ # Compatibility with both Solr and Whoosh backends.  
+ mlt = None  
+ hexdigests = None  
+  
+ if self.backend.__class__.__name__ == 'SolrBackend':  
+ query_result, mlt, hexdigests = self.backend.query(**query_parameters)  
+ else:  
+ query_result = self.backend.query(**query_parameters)  

for post_processor in self.result_post_processors:  
post_processor.post_process(query_result)  

query_result.debug["api_parameters"] = query_parameters  
- return query_result  
+ return query_result, mlt, hexdigests  

def start_operation(self):  
return self.backend.start_operation()  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/templates/bhsearch.html  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/templates/bhsearch.html?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/templates/bhsearch.html (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/templates/bhsearch.html Wed Aug 13 16:08:04 2014  
@@ -166,6 +166,10 @@  
<py:if test="result.author"><span class="author" i18n:msg="author">By ${format_author(result.author)}</span> &mdash;</py:if>  
<span class="date">${result.date}</span>  
</dd>  
+ <!-- Include the template that displays More Like This results for each result (only for when Solr backend is being used, not Whoosh). -->  
+ <dd py:if="mlt and hexdigests">  
+ <xi:include py:with="doc = result; doc_mlt = mlt[result.unique_id]; hexdigest = hexdigests[result.unique_id];" href="bh_more_like_this.html" />  
+ </dd>  
</py:for>  
</dl>  
</py:if>  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/web_ui.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/web_ui.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/web_ui.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_search/bhsearch/web_ui.py Wed Aug 13 16:08:04 2014  
@@ -338,7 +338,7 @@ class BloodhoundSearchModule(Component):  
# compatibility with legacy search  
req.search_query = request_context.parameters.query  

- query_result = BloodhoundSearchApi(self.env).query(  
+ query_result, mlt, hexdigests = BloodhoundSearchApi(self.env).query(  
request_context.parameters.query,  
pagenum=request_context.page,  
pagelen=request_context.pagelen,  
@@ -350,6 +350,11 @@ class BloodhoundSearchModule(Component):  
context=request_context,  
)  

+ # Needed for showing More Like This results in Genshi  
+ # templates.  
+ request_context.data['mlt'] = mlt  
+ request_context.data['hexdigests'] = hexdigests  
+  
request_context.process_results(query_result)  
return self._return_data(req, request_context.data)  


Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/README  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/README?rev=1617747&view=auto  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/README (added)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/README Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,29 @@  
+= Plugin to add Apache Solr as a search alternative for Apache Bloodhound =  
+  
+== Description ==  
+  
+The plugin enhances Apache Bloodhound with Apache Solr functionality, namely  
+it allows using Apache Solr as a search alternative.  
+  
+== Dependencies ==  
+  
+This plugin depends on the following components to be installed:  
+  
+ - [http:trac.edgewall.org Trac] ,,Since version ''' 1.0 ''',, .  
+ - [http://lucene.apache.org/solr/index.html Apache Solr] ,,Version ''' 4.7.2 ''',, .  
+ - [http://lxml.de/ Lxml]  
+ - [https://github.com/tow/sunburnt Sunburnt] ,,Since version '''0.6''',, .  
+ - [https://code.google.com/p/httplib2/ Httplib2]  
+  
+  
+== Latest Version ==  
+  
+  
+== Installation ==  
+  
+  
+== Licensing ==  
+  
+  
+== Contacts ==  
+  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/__init__.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/__init__.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/__init__.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/__init__.py Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,18 @@  
+# -*- 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.  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py Wed Aug 13 16:08:04 2014  
@@ -1,6 +1,25 @@  
+# -*- 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.  
+  
from trac.core import Component, implements  
-from bhsolr.schema import SolrSchema  
from trac.admin import IAdminCommandProvider  
+from bhsolr.schema import SolrSchema  

class BloodhoundSolrAdmin(Component):  


Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py Wed Aug 13 16:08:04 2014  
@@ -1,169 +1,198 @@  
+# -*- 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.  
+  
+import os  
+  
from lxml import etree  
-from bhsearch.whoosh_backend import WhooshBackend  
+  
from trac.core import Component, implements, TracError  
-import os  
+from bhsearch.whoosh_backend import WhooshBackend  
+from bhsolr.solr_backend import SolrBackend  

class SolrSchema(Component):  
- instance = None  

- REQUIRED_FIELDS = {"id": True,  
- "unique_id": True,  
- "type": True}  
-  
- FIELDS_TYPE_DICT = {"ID": "string",  
- "DATETIME": "date",  
- "KEYWORD": "string",  
- "TEXT": "text_general"  
- }  
-  
- def __init__(self):  
- self.schema = WhooshBackend.SCHEMA  
- self.schema_element = etree.Element("schema")  
- self.schema_element.set("name", "Bloodhound Solr Schema")  
- self.schema_element.set("version", "1")  
-  
- self.path = None  
- self.fields_element = etree.SubElement(self.schema_element, "fields")  
- self.unique_key_element = etree.SubElement(self.schema_element, "uniqueKey")  
- self.unique_key_element.text = "unique_id"  
-  
- version_field = etree.SubElement(self.fields_element, "field")  
- version_field.set("name", "_version_")  
- version_field.set("type", "long")  
- version_field.set("indexed", "true")  
- version_field.set("stored", "true")  
-  
- root_field = etree.SubElement(self.fields_element, "field")  
- root_field.set("name", "_root_")  
- root_field.set("type", "string")  
- root_field.set("indexed", "true")  
- root_field.set("stored", "false")  
-  
- stored_name = etree.SubElement(self.fields_element, "field")  
- stored_name.set("name", "_stored_name")  
- stored_name.set("type", "string")  
- stored_name.set("indexed", "true")  
- stored_name.set("stored", "true")  
- stored_name.set("required", "false")  
- stored_name.set("multivalued", "false")  
-  
- # @classmethod  
- # def getInstance(self, env):  
- # if not self.instance:  
- # self.instance = SolrSchema(env)  
- # return self.instance  
-  
- def generate_schema(self, path=None):  
- if not path:  
- path = os.getcwd()  
-  
- self.add_all_fields()  
- self.add_type_definitions()  
- doc = etree.ElementTree(self.schema_element)  
-  
- self.path = os.path.join(path, 'schema.xml')  
-  
- out_file = open(os.path.join(path, 'schema.xml'), 'w')  
- doc.write(out_file, xml_declaration=True, encoding='UTF-8', pretty_print=True)  
- out_file.close()  
-  
- def add_field(self, field_name, name_attr, type_attr, indexed_attr, stored_attr, required_attr, multivalued_attr):  
- field = etree.SubElement(self.fields_element, field_name)  
- field.set("name", name_attr)  
- field.set("type", type_attr)  
- field.set("indexed", indexed_attr)  
- field.set("stored", stored_attr)  
- field.set("required", required_attr)  
- field.set("multivalued", multivalued_attr)  
-  
- def add_all_fields(self):  
- for (field_name, field_attrs) in self.schema.items():  
- type_attr = SolrSchema.FIELDS_TYPE_DICT[str(field_attrs.__class__.__name__)]  
- indexed_attr = str(field_attrs.indexed).lower()  
- stored_attr = str(field_attrs.stored).lower()  
- if field_name in SolrSchema.REQUIRED_FIELDS:  
- required_attr = "true"  
- else:  
- required_attr = "false"  
-  
- self.add_field("field", field_name, type_attr, indexed_attr, stored_attr, required_attr, "false")  
-  
-  
- def add_type_definitions(self):  
- self.types_element = etree.SubElement(self.schema_element, "types")  
- self._add_string_type_definition()  
- self._add_text_general_type_definition()  
- self._add_date_type_definition()  
- self._add_long_type_definition()  
- self._add_lowercase_type_definition()  
-  
-  
- def _add_string_type_definition(self):  
- field_type = etree.SubElement(self.types_element, "fieldType")  
- field_type.set("name", "string")  
- field_type.set("class", "solr.StrField")  
- field_type.set("sortMissingLast", "true")  
-  
-  
- def _add_text_general_type_definition(self):  
- field_type = etree.SubElement(self.types_element, "fieldType")  
- field_type.set("name", "text_general")  
- field_type.set("class", "solr.TextField")  
- field_type.set("positionIncrementGap", "100")  
-  
- analyzer_index = etree.SubElement(field_type, "analyzer")  
- analyzer_index.set("type", "index")  
-  
- tokenizer_index = etree.SubElement(analyzer_index, "tokenizer")  
- tokenizer_index.set("class", "solr.StandardTokenizerFactory")  
- filter1 = etree.SubElement(analyzer_index, "filter")  
- filter1.set("class", "solr.StopFilterFactory")  
- filter1.set("ignoreCase", "true")  
- filter1.set("words", "stopwords.txt")  
-  
- filter2 = etree.SubElement(analyzer_index, "filter")  
- filter2.set("class", "solr.LowerCaseFilterFactory")  
-  
- analyzer_query = etree.SubElement(field_type, "analyzer")  
- analyzer_query.set("type", "query")  
- tokenizer_query = etree.SubElement(analyzer_query, "tokenizer")  
- tokenizer_query.set("class", "solr.StandardTokenizerFactory")  
- filter3 = etree.SubElement(analyzer_query, "filter")  
- filter3.set("class", "solr.StopFilterFactory")  
- filter3.set("ignoreCase", "true")  
- filter3.set("words", "stopwords.txt")  
-  
- filter4 = etree.SubElement(analyzer_query, "filter")  
- filter4.set("class", "solr.SynonymFilterFactory")  
- filter4.set("synonyms", "synonyms.txt")  
- filter4.set("ignoreCase", "true")  
- filter4.set("expand", "true")  
-  
- filter5 = etree.SubElement(analyzer_query, "filter")  
- filter5.set("class", "solr.LowerCaseFilterFactory")  
-  
- def _add_date_type_definition(self):  
- field_type = etree.SubElement(self.types_element, "fieldType")  
- field_type.set("name", "date")  
- field_type.set("class", "solr.TrieDateField")  
- field_type.set("precisionStep", "0")  
- field_type.set("positionIncrementGap", "0")  
-  
- def _add_long_type_definition(self):  
- field_type = etree.SubElement(self.types_element, "fieldType")  
- field_type.set("name", "long")  
- field_type.set("class", "solr.TrieLongField")  
- field_type.set("precisionStep", "0")  
- field_type.set("positionIncrementGap", "0")  
-  
- def _add_lowercase_type_definition(self):  
- field_type = etree.SubElement(self.types_element, "fieldType")  
- field_type.set("name", "lowercase")  
- field_type.set("class", "solr.TextField")  
- field_type.set("positionIncrementGap", "100")  
-  
- analyzer = etree.SubElement(field_type, "analyzer")  
- tokenizer = etree.SubElement(analyzer, "tokenizer")  
- tokenizer.set("class", "solr.KeywordTokenizerFactory")  
- filter_lowercase = etree.SubElement(analyzer, "filter")  
- filter_lowercase.set("class", "solr.LowerCaseFilterFactory")  
+ REQUIRED_FIELDS = {  
+ "id": True,  
+ "unique_id": True,  
+ "type": True  
+ }  
+  
+ FIELDS_TYPE_DICT = {  
+ "ID": "string",  
+ "DATETIME": "date",  
+ "KEYWORD": "string",  
+ "TEXT": "text_general"  
+ }  
+  
+ def __init__(self):  
+ self.path = None  
+ self.schema = WhooshBackend.SCHEMA  
+ self.schema_element = etree.Element("schema")  
+ self.schema_element.set("name", "Bloodhound Solr Schema")  
+ self.schema_element.set("version", "1")  
+  
+ self.fields_element = etree.SubElement(self.schema_element, "fields")  
+  
+ self.unique_key_element = etree.SubElement(self.schema_element,  
+ "uniqueKey")  
+ self.unique_key_element.text = SolrBackend.UNIQUE_ID  
+  
+ version_field = etree.SubElement(self.fields_element, "field")  
+ version_field.set("name", "_version_")  
+ version_field.set("type", "long")  
+ version_field.set("indexed", "true")  
+ version_field.set("stored", "true")  
+ version_field.set("multiValued", "false")  
+  
+ root_field = etree.SubElement(self.fields_element, "field")  
+ root_field.set("name", "_root_")  
+ root_field.set("type", "string")  
+ root_field.set("indexed", "true")  
+ root_field.set("stored", "false")  
+  
+ stored_name = etree.SubElement(self.fields_element, "field")  
+ stored_name.set("name", "_stored_name")  
+ stored_name.set("type", "string")  
+ stored_name.set("indexed", "true")  
+ stored_name.set("stored", "true")  
+ stored_name.set("required", "false")  
+ stored_name.set("multiValued", "false")  
+  
+ def generate_schema(self, path=None):  
+ if not path:  
+ path = os.getcwd()  
+ self.path = os.path.join(path, 'schema.xml')  
+  
+ self.add_all_fields()  
+ self.add_type_definitions()  
+  
+ doc = etree.ElementTree(self.schema_element)  
+ out_file = open(os.path.join(path, 'schema.xml'), 'w')  
+ doc.write(out_file, xml_declaration=True, encoding='UTF-8',  
+ pretty_print=True)  
+ out_file.close()  
+  
+ def add_field(  
+ self, field_name, name_attr, type_attr, indexed_attr,  
+ stored_attr, required_attr, multivalued_attr):  
+ field = etree.SubElement(self.fields_element, field_name)  
+ field.set("name", name_attr)  
+ field.set("type", type_attr)  
+ field.set("indexed", indexed_attr)  
+ field.set("stored", stored_attr)  
+ field.set("required", required_attr)  
+ field.set("multiValued", multivalued_attr)  
+  
+ def add_all_fields(self):  
+ for (field_name, field_attrs) in self.schema.items():  
+ class_name = str(field_attrs.__class__.__name__)  
+ type_attr = self.FIELDS_TYPE_DICT[class_name]  
+ indexed_attr = str(field_attrs.indexed).lower()  
+ stored_attr = str(field_attrs.stored).lower()  
+  
+ if field_name in self.REQUIRED_FIELDS:  
+ required_attr = "true"  
+ else:  
+ required_attr = "false"  
+  
+ self.add_field("field", field_name, type_attr, indexed_attr,  
+ stored_attr, required_attr, "false")  
+  
+ def add_type_definitions(self):  
+ self.types_element = etree.SubElement(self.schema_element, "types")  
+ self._add_string_type_definition()  
+ self._add_text_general_type_definition()  
+ self._add_date_type_definition()  
+ self._add_long_type_definition()  
+ self._add_lowercase_type_definition()  
+  
+ def _add_string_type_definition(self):  
+ field_type = etree.SubElement(self.types_element, "fieldType")  
+ field_type.set("name", "string")  
+ field_type.set("class", "solr.StrField")  
+ field_type.set("sortMissingLast", "true")  
+  
+ def _add_text_general_type_definition(self):  
+ field_type = etree.SubElement(self.types_element, "fieldType")  
+ field_type.set("name", "text_general")  
+ field_type.set("class", "solr.TextField")  
+ field_type.set("positionIncrementGap", "100")  
+  
+ analyzer_index = etree.SubElement(field_type, "analyzer")  
+ analyzer_index.set("type", "index")  
+  
+ tokenizer_index = etree.SubElement(analyzer_index, "tokenizer")  
+ tokenizer_index.set("class", "solr.StandardTokenizerFactory")  
+  
+ analyzer_index_filter_s = etree.SubElement(analyzer_index, "filter")  
+ analyzer_index_filter_s.set("class", "solr.StopFilterFactory")  
+ analyzer_index_filter_s.set("ignoreCase", "true")  
+ analyzer_index_filter_s.set("words", "stopwords.txt")  
+  
+ analyzer_index_filter_l = etree.SubElement(analyzer_index, "filter")  
+ analyzer_index_filter_l.set("class", "solr.LowerCaseFilterFactory")  
+  
+ analyzer_query = etree.SubElement(field_type, "analyzer")  
+ analyzer_query.set("type", "query")  
+  
+ tokenizer_query = etree.SubElement(analyzer_query, "tokenizer")  
+ tokenizer_query.set("class", "solr.StandardTokenizerFactory")  
+  
+ analyzer_query_filter_s = etree.SubElement(analyzer_query, "filter")  
+ analyzer_query_filter_s.set("class", "solr.StopFilterFactory")  
+ analyzer_query_filter_s.set("ignoreCase", "true")  
+ analyzer_query_filter_s.set("words", "stopwords.txt")  
+  
+ analyzer_query_filter_syn = etree.SubElement(analyzer_query, "filter")  
+ analyzer_query_filter_syn.set("class", "solr.SynonymFilterFactory")  
+ analyzer_query_filter_syn.set("synonyms", "synonyms.txt")  
+ analyzer_query_filter_syn.set("ignoreCase", "true")  
+ analyzer_query_filter_syn.set("expand", "true")  
+  
+ analyzer_query_filter_l = etree.SubElement(analyzer_query, "filter")  
+ analyzer_query_filter_l.set("class", "solr.LowerCaseFilterFactory")  
+  
+ def _add_date_type_definition(self):  
+ field_type = etree.SubElement(self.types_element, "fieldType")  
+ field_type.set("name", "date")  
+ field_type.set("class", "solr.TrieDateField")  
+ field_type.set("precisionStep", "0")  
+ field_type.set("positionIncrementGap", "0")  
+  
+ def _add_long_type_definition(self):  
+ field_type = etree.SubElement(self.types_element, "fieldType")  
+ field_type.set("name", "long")  
+ field_type.set("class", "solr.TrieLongField")  
+ field_type.set("precisionStep", "0")  
+ field_type.set("positionIncrementGap", "0")  
+  
+ def _add_lowercase_type_definition(self):  
+ field_type = etree.SubElement(self.types_element, "fieldType")  
+ field_type.set("name", "lowercase")  
+ field_type.set("class", "solr.TextField")  
+ field_type.set("positionIncrementGap", "100")  
+  
+ analyzer = etree.SubElement(field_type, "analyzer")  
+  
+ tokenizer = etree.SubElement(analyzer, "tokenizer")  
+ tokenizer.set("class", "solr.KeywordTokenizerFactory")  
+  
+ analyzer_filter_l = etree.SubElement(analyzer, "filter")  
+ analyzer_filter_l.set("class", "solr.LowerCaseFilterFactory")  
+  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/__init__.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/__init__.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/__init__.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/__init__.py Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,18 @@  
+# -*- 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.  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/changeset_search.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/changeset_search.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/changeset_search.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/changeset_search.py Wed Aug 13 16:08:04 2014  
@@ -1,17 +1,36 @@  
-from bhsearch.search_resources.base import BaseIndexer  
+# -*- 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.  
+  
from trac.versioncontrol.api import RepositoryManager  
+from bhsearch.search_resources.base import BaseIndexer  
from bhsearch.search_resources.changeset_search import ChangesetIndexer  

class ChangesetSearchModel(BaseIndexer):  

- def get_entries_for_index(self):  
- repository_manager = RepositoryManager(self.env)  
- for repository in repository_manager.get_real_repositories():  
- rev = repository.oldest_rev  
- stop = repository.youngest_rev  
- while True:  
- changeset = repository.get_changeset(rev)  
- yield ChangesetIndexer(self.env).build_doc(changeset)  
- if rev == stop:  
- break  
- rev = repository.next_rev(rev)  
+ def get_entries_for_index(self):  
+ repository_manager = RepositoryManager(self.env)  
+ for repository in repository_manager.get_real_repositories():  
+ rev = repository.oldest_rev  
+ stop = repository.youngest_rev  
+ while True:  
+ changeset = repository.get_changeset(rev)  
+ yield ChangesetIndexer(self.env).build_doc(changeset)  
+ if rev == stop:  
+ break  
+ rev = repository.next_rev(rev)  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/milestone_search.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/milestone_search.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/milestone_search.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/milestone_search.py Wed Aug 13 16:08:04 2014  
@@ -1,9 +1,28 @@  
-from bhsearch.search_resources.base import BaseIndexer  
+# -*- 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.  
+  
from trac.ticket import Milestone  
+from bhsearch.search_resources.base import BaseIndexer  
from bhsearch.search_resources.milestone_search import MilestoneIndexer  

class MilestoneSearchModel(BaseIndexer):  

- def get_entries_for_index(self):  
- for milestone in Milestone.select(self.env, include_completed=True):  
- yield MilestoneIndexer(self.env).build_doc(milestone)  
+ def get_entries_for_index(self):  
+ for milestone in Milestone.select(self.env, include_completed=True):  
+ yield MilestoneIndexer(self.env).build_doc(milestone)  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/ticket_search.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/ticket_search.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/ticket_search.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/ticket_search.py Wed Aug 13 16:08:04 2014  
@@ -1,27 +1,46 @@  
+# -*- 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.  
+  
from trac.ticket.model import Ticket  
-from bhsearch.search_resources.ticket_search import TicketIndexer  
from trac.core import Component, implements, TracError  
+from bhsearch.search_resources.ticket_search import TicketIndexer  
from bhsearch.search_resources.base import BaseIndexer  

class TicketSearchModel(BaseIndexer):  

- def _fetch_tickets(self, **kwargs):  
- for ticket_id in self._fetch_ids(**kwargs):  
- yield Ticket(self.env, ticket_id)  
-  
- def _fetch_ids(self, **kwargs):  
- sql = "SELECT id FROM ticket"  
- args = []  
- conditions = []  
- for key, value in kwargs.iteritems():  
- args.append(value)  
- conditions.append(key + "=%s")  
- if conditions:  
- sql = sql + " WHERE " + " AND ".join(conditions)  
- for row in self.env.db_query(sql, args):  
- yield int(row[0])  
-  
- def get_entries_for_index(self):  
- for ticket in self._fetch_tickets():  
- yield TicketIndexer(self.env).build_doc(ticket)  
+ def _fetch_tickets(self, **kwargs):  
+ for ticket_id in self._fetch_ids(**kwargs):  
+ yield Ticket(self.env, ticket_id)  
+  
+ def _fetch_ids(self, **kwargs):  
+ sql = "SELECT id FROM ticket"  
+ args = []  
+ conditions = []  
+ for key, value in kwargs.iteritems():  
+ args.append(value)  
+ conditions.append(key + "=%s")  
+ if conditions:  
+ sql = sql + " WHERE " + " AND ".join(conditions)  
+ for row in self.env.db_query(sql, args):  
+ yield int(row[0])  
+  
+ def get_entries_for_index(self):  
+ for ticket in self._fetch_tickets():  
+ yield TicketIndexer(self.env).build_doc(ticket)  


Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/wiki_search.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/wiki_search.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/wiki_search.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/search_resources/wiki_search.py Wed Aug 13 16:08:04 2014  
@@ -1,11 +1,30 @@  
+# -*- 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.  
+  
from trac.wiki import WikiSystem, WikiPage  
from bhsearch.search_resources.wiki_search import WikiIndexer  
from bhsearch.search_resources.base import BaseIndexer  

class WikiSearchModel(BaseIndexer):  

- def get_entries_for_index(self):  
- page_names = WikiSystem(self.env).get_pages()  
- for page_name in page_names:  
- page = WikiPage(self.env, page_name)  
- yield WikiIndexer(self.env).build_doc(page)  
+ def get_entries_for_index(self):  
+ page_names = WikiSystem(self.env).get_pages()  
+ for page_name in page_names:  
+ page = WikiPage(self.env, page_name)  
+ yield WikiIndexer(self.env).build_doc(page)  

Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py?rev=1617747&view=auto  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py (added)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,276 @@  
+# -*- 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.  
+  
+import re  
+import hashlib  
+  
+from math import ceil  
+from datetime import datetime  
+from contextlib import contextmanager  
+from sunburnt import SolrInterface  
+  
+from bhsearch import BHSEARCH_CONFIG_SECTION  
+from bhsearch.api import ISearchBackend, SCORE, QueryResult  
+from bhsearch.query_parser import DefaultQueryParser  
+from bhsearch.search_resources.ticket_search import TicketIndexer  
+from multiproduct.env import ProductEnvironment  
+from trac.core import Component, implements, TracError  
+from trac.config import Option  
+from trac.ticket.model import Ticket  
+from trac.ticket.api import TicketSystem  
+from trac.util.datefmt import utc  
+  
+  
+class SolrBackend(Component):  
+ implements(ISearchBackend)  
+  
+ UNIQUE_ID = "unique_id"  
+  
+ HIGHLIGHTABLE_FIELDS = {  
+ "unique_id" : True,  
+ "id" : True,  
+ "type" : True,  
+ "product" : True,  
+ "milestone" : True,  
+ "author" : True,  
+ "component" : True,  
+ "status" : True,  
+ "resolution" : True,  
+ "keywords" : True,  
+ "summary" : True,  
+ "content" : True,  
+ "changes" : True,  
+ "owner" : True,  
+ "repository" : True,  
+ "revision" : True,  
+ "message" : True,  
+ "name" : True  
+ }  
+  
+ server_url = Option(  
+ BHSEARCH_CONFIG_SECTION,  
+ 'solr_server_url',  
+ doc="""Url of the server running Solr instance.""",  
+ doc_domain='bhsearch')  
+  
+ def __init__(self):  
+ self.solr_interface = SolrInterface(str(self.server_url))  
+  
+ def add_doc(self, doc, operation_context=None):  
+ self._reformat_doc(doc)  
+ doc[self.UNIQUE_ID] = self._create_unique_id(doc.get("product", ''),  
+ doc["type"], doc["id"])  
+ self.solr_interface.add(doc)  
+ self.solr_interface.commit()  
+  
+ def delete_doc(product, doc_type, doc_id, operation_context=None):  
+ unique_id = self._create_unique_id(product, doc_type, doc_id)  
+ self.solr_interface.delete(unique_id)  
+  
+ def optimize(self):  
+ self.solr_interface.optimize()  
+  
+ def query(  
+ self, query, query_string, sort = None, fields = None,  
+ filter = None, facets = None, pagenum = 1, pagelen = 20,  
+ highlight = False, highlight_fields = None, context = None):  
+  
+ if not query_string:  
+ query_string = "*.*"  
+  
+ final_query_chain = self._create_query_chain(query, query_string)  
+ solr_query = self.solr_interface.query(final_query_chain)  
+ faceted_solr_query = solr_query.facet_by(facets)  
+ highlighted_solr_query = faceted_solr_query.highlight(  
+ self.HIGHLIGHTABLE_FIELDS)  
+  
+ start = 0 if pagenum == 1 else pagelen * pagenum  
+ paginated_solr_query = highlighted_solr_query.paginate(  
+ start=start, rows=pagelen)  
+ results = paginated_solr_query.execute()  
+  
+ mlt, hexdigests = self.query_more_like_this(paginated_solr_query,  
+ fields="type", mindf=1,  
+ mintf=1)  
+  
+ query_result = self._create_query_result(highlighted_solr_query,  
+ results, fields, pagenum,  
+ pagelen)  
+ return query_result, mlt, hexdigests  
+  
+ def query_more_like_this(self, query_chain, **kwargs):  
+ mlt_results = query_chain.mlt(**kwargs).execute().more_like_these  
+ mlt_dict = {}  
+ hexdigests = {}  
+  
+ for doc, results in mlt_results.iteritems():  
+ hexdigest = hashlib.md5(doc).hexdigest()  
+ hexdigests[doc] = hexdigest  
+  
+ for mlt_doc in results.docs:  
+ if doc not in mlt_dict:  
+ mlt_dict[doc] = [self._process_doc(mlt_doc)]  
+ else:  
+ mlt_dict[doc].append(self._process_doc(mlt_doc))  
+  
+ return mlt_dict, hexdigests  
+  
+ def _process_doc(self, doc):  
+ ui_doc = dict(doc)  
+  
+ if doc.get('product'):  
+ env = ProductEnvironment(self.env, doc['product'])  
+ product_href = ProductEnvironment.resolve_href(env, self.env)  
+ ui_doc["href"] = product_href(doc['type'], doc['id'])  
+ else:  
+ ui_doc["href"] = self.env.href(doc['type'], doc['id'])  
+  
+ ui_doc['title'] = str(doc['type'] + ": " + doc['_stored_name']).title()  
+  
+ return ui_doc  
+  
+ def _create_query_result(  
+ self, query, results, fields, pagenum, pagelen):  
+ total_num, total_page_count, page_num, offset = \  
+ self._prepare_query_result_attributes(query, results,  
+ pagenum, pagelen)  
+  
+ query_results = QueryResult()  
+ query_results.hits = total_num  
+ query_results.total_page_count = total_page_count  
+ query_results.page_number = page_num  
+ query_results.offset = offset  
+  
+ docs = []  
+ highlighting = []  
+  
+ for retrieved_record in results:  
+ result_doc = self._process_record(fields, retrieved_record)  
+ docs.append(result_doc)  
+  
+ result_highlights = dict(retrieved_record['solr_highlights'])  
+  
+ highlighting.append(result_highlights)  
+ query_results.docs = docs  
+ query_results.highlighting = highlighting  
+  
+ return query_results  
+  
+ def _create_query_chain(self, query, query_string):  
+ matches = re.findall(re.compile(r'([\w\*]+)'), query_string)  
+ tokens = set([match for match in matches])  
+  
+ final_query_chain = None  
+ for token in tokens:  
+ token_query_chain = self._search_fields_for_token(token)  
+ if final_query_chain is None:  
+ final_query_chain = token_query_chain  
+ else:  
+ final_query_chain |= token_query_chain  
+  
+ return final_query_chain  
+  
+ def _process_record(self, fields, retrieved_record):  
+ result_doc = dict()  
+ if fields:  
+ for field in fields:  
+ if field in retrieved_record:  
+ result_doc[field] = retrieved_record[field]  
+ else:  
+ for key, value in retrieved_record.items():  
+ result_doc[key] = value  
+  
+ for key, value in result_doc.iteritems():  
+ result_doc[key] = self._from_whoosh_format(value)  
+  
+ return result_doc  
+  
+ def _from_whoosh_format(self, value):  
+ if isinstance(value, datetime):  
+ value = utc.localize(value)  
+ return value  
+  
+ def _prepare_query_result_attributes(  
+ self, query, results, pagenum, pagelen):  
+ results_total_num = query.execute().result.numFound  
+ total_page_count = int(ceil(results_total_num / pagelen))  
+ pagenum = min(total_page_count, pagenum)  
+  
+ offset = (pagenum-1) * pagelen  
+ if (offset+pagelen) > results_total_num:  
+ pagelen = results_total_num - offset  
+  
+ return results_total_num, total_page_count, pagenum, offset  
+  
+ def is_index_outdated(self):  
+ return False  
+  
+ def recreate_index(self):  
+ return True  
+  
+ @contextmanager  
+ def start_operation(self):  
+ yield  
+  
+ def _search_fields_for_token(self, token):  
+ q_chain = None  
+ field_boosts = DefaultQueryParser(self.env).field_boosts  
+  
+ for field, boost in field_boosts.iteritems():  
+ if field != 'query_suggestion_basket' and field != 'relations':  
+ field_token_dict = {field: token}  
+ if q_chain is None:  
+ q_chain = self.solr_interface.Q(**field_token_dict)**boost  
+ else:  
+ q_chain |= self.solr_interface.Q(**field_token_dict)**boost  
+  
+ return q_chain  
+  
+ def _reformat_doc(self, doc):  
+ for key, value in doc.items():  
+ if key is None:  
+ del doc[None]  
+ elif value is None:  
+ del doc[key]  
+ elif isinstance(value, basestring) and value == "":  
+ del doc[key]  
+ else:  
+ doc[key] = self._to_whoosh_format(value)  
+  
+ def _to_whoosh_format(self, value):  
+ if isinstance(value, basestring):  
+ value = unicode(value)  
+ elif isinstance(value, datetime):  
+ value = self._convert_date_to_tz_naive_utc(value)  
+ return value  
+  
+ def _convert_date_to_tz_naive_utc(self, value):  
+ if value.tzinfo:  
+ utc_time = value.astimezone(utc)  
+ value = utc_time.replace(tzinfo=None)  
+ return value  
+  
+ def _create_unique_id(self, product, doc_type, doc_id):  
+ if product:  
+ return u"%s:%s:%s" % (product, doc_type, doc_id)  
+ else:  
+ return u"%s:%s" % (doc_type, doc_id)  
+  
+  
+  

Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py?rev=1617747&view=auto  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py (added)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,18 @@  
+try:  
+ import unittest2 as unittest  
+except ImportError:  
+ import unittest  
+  
+from bhsolr.tests import (  
+ backend  
+)  
+  
+  
+def suite():  
+ test_suite = unittest.TestSuite()  
+ return test_suite  
+  
+if __name__ == '__main__':  
+ unittest.main(defaultTest='suite')  
+else:  
+ test_suite = suite()  

Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/backend.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/backend.py?rev=1617747&view=auto  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/backend.py (added)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/backend.py Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,21 @@  
+import unittest  
+  
+from trac.test import EnvironmentStub, Mock, MockPerm  
+from trac.util.datefmt import utc  
+from trac.web.chrome import Chrome  
+from bhsearch.tests.base import BaseBloodhoundSearchTest  
+  
+class SolrBackendTestCase(BaseBloodhoundSearchTest):  
+ def setUp(self):  
+ super(SolrBackendTestCase, self).setUp()  
+ self.solr_backend = SolrBackend(self.env)  
+ # self.parser = DefaultQueryParser(self.env)  
+  
+def suite():  
+ suite = unittest.TestSuite()  
+ suite.addTest(unittest.makeSuite(SolrBackendTestCase))  
+ return suite  
+  
+  
+if __name__ == '__main__':  
+ unittest.main(defaultTest='suite')  

Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py?rev=1617747&view=auto  
==============================================================================  
(empty)  

Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/search_resources/__init__.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/search_resources/__init__.py?rev=1617747&view=auto  
==============================================================================  
(empty)  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py Wed Aug 13 16:08:04 2014  
@@ -1,34 +1,59 @@  
+# -*- 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.  
+  
from setuptools import setup, find_packages  

-PKG_INFO = {'bhsolr': ['schemadoc/*.xml'],  
- 'bhsolr.search_resources' : [],  
- }  

+PKG_INFO = {  
+ 'bhsolr': ['../README', '../TESTING_README'],  
+ 'bhsolr.search_resources' : [],  
+ 'bhsolr.tests': ['*.*'],  
+ 'bhsolr.tests.search_resources': ['*.*'],  
+ }  


ENTRY_POINTS = {  
- 'trac.plugins': [  
- 'bhsolr.admin = bhsolr.admin',  
- 'bhsolr.schema = bhsolr.schema',  
- 'bhsolr.backend = bhsolr.backend',  
- 'bhsolr.web_ui = bhsolr.web_ui',  
- 'bhsolr.search_resources.ticket_search = bhsolr.search_resources.ticket_search',  
- 'bhsolr.search_resources.milestone_search = bhsolr.search_resources.milestone_search',  
- 'bhsolr.search_resources.changeset_search = bhsolr.search_resources.changeset_search',  
- 'bhsolr.search_resources.wiki_search = bhsolr.search_resources.wiki_search'  
- ],}  
+ 'trac.plugins': [  
+ 'bhsolr.admin = bhsolr.admin',  
+ 'bhsolr.schema = bhsolr.schema',  
+ 'bhsolr.solr_backend = bhsolr.solr_backend',  
+ 'bhsolr.search_resources.ticket_search = \  
+ bhsolr.search_resources.ticket_search',  
+ 'bhsolr.search_resources.milestone_search = \  
+ bhsolr.search_resources.milestone_search',  
+ 'bhsolr.search_resources.changeset_search = \  
+ bhsolr.search_resources.changeset_search',  
+ 'bhsolr.search_resources.wiki_search = bhsolr.search_resources.wiki_search'  
+ ],}  
+  

setup(  
- name = 'BloodhoundSolrPlugin',  
- version = '0.1',  
- description = "Apache Solr support for Apache(TM) Bloodhound.",  
- author = "Apache Bloodhound",  
- license = "Apache License v2",  
- url = "http://bloodhound.apache.org/",  
- requires = ['trac', 'lxml', 'sunburnt', 'httplib2'],  
- packages = find_packages(),  
- package_data = PKG_INFO,  
- include_package_data=True,  
- entry_points = ENTRY_POINTS,  
- test_suite='bhsolr.tests.test_suite',  
-)  
+ name = 'BloodhoundSolrPlugin',  
+ version = '0.1',  
+ description = "Apache Solr support for Apache(TM) Bloodhound.",  
+ author = "Apache Bloodhound",  
+ license = "Apache License v2",  
+ url = "http://bloodhound.apache.org/",  
+ requires = ['trac', 'lxml', 'sunburnt', 'httplib2'],  
+ packages = find_packages(),  
+ package_data = PKG_INFO,  
+ include_package_data=True,  
+ entry_points = ENTRY_POINTS,  
+ test_suite='bhsolr.tests.test_suite',  
+ )  

Added: bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html?rev=1617747&view=auto  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html (added)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html Wed Aug 13 16:08:04 2014  
@@ -0,0 +1,32 @@  
+<!DOCTYPE html  
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  
+<html xmlns="http://www.w3.org/1999/xhtml"  
+ xmlns:xi="http://www.w3.org/2001/XInclude"  
+ xmlns:py="http://genshi.edgewall.org/"  
+ xmlns:i18n="http://genshi.edgewall.org/i18n">  
+  
+ <body>  
+ <div class="panel-group" id="accordion${doc.unique_id}">  
+ <div class="panel panel-default">  
+ <div class="panel-heading">  
+ <h4 class="panel-title">  
+ <a class="btn" data-toggle="collapse" data-parent="#accordion${doc.unique_id}" href="#collapse${hexdigest}">  
+ More Like This  
+ </a>  
+ </h4>  
+ </div>  
+ <div id="collapse${hexdigest}" class="panel-collapse collapse out">  
+ <div class="panel-body">  
+ <dl>  
+ <py:for each="result in doc_mlt">  
+ <dt><a href="${result.href}">${result.title}</a></dt>  
+ </py:for>  
+ </dl>  
+ </div>  
+ </div>  
+ </div>  
+ </div>  
+  
+ </body>  
+</html>  

Modified: bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/theme.py  
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/theme.py?rev=1617747&r1=1617746&r2=1617747&view=diff  
==============================================================================  
--- bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/theme.py (original)  
+++ bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/theme.py Wed Aug 13 16:08:04 2014  
@@ -353,6 +353,7 @@ class BloodhoundTheme(ThemeBase):  
req.search_query = data.get('query')  
# Context nav  
prevnext_nav(req, _('Previous'), _('Next'))  
+ self._add_more_like_this(req, template, data, content_type, is_active)  
# Breadcrumbs nav  
data['resourcepath_template'] = 'bh_path_search.html'  

@@ -451,6 +452,11 @@ class BloodhoundTheme(ThemeBase):  
if isinstance(req.perm.env, ProductEnvironment):  
data['resourcepath_template'] = 'bh_path_general.html'  

+ def _add_more_like_this(self, req, template, data,  
+ content_type, is_active):  
+ """Adds a template for displaying More Like This results."""  
+ data['resourcepath_template'] = 'bh_more_like_this.html'  
+  
def _modify_product_list(self, req, template, data, content_type,  
is_active):  
"""Transform products list into media list by adding