Commit d47df833 authored by Kazuhiko Shiozaki's avatar Kazuhiko Shiozaki

use fulltext search in title and description.

* to quickly setup catalog_full_text table, you can use the following SQL.

  REPLACE INTO catalog_full_text SELECT uid, title, description FROM catalog;

* non fulltext queries like '=abc', '>abc', '%abc%' are supported.

* now erp5_full_text_mroonga_catalog is used for unit tests thus I recommend using it instead of erp5_full_text_myisam_catalog.

* to migrate existing MyISAM full_text table into Mroonga, you can use the following SQL.

  ALTER TABLE full_text DROP KEY SearchableText,
    ENGINE = mroonga,
    ADD FULLTEXT KEY SearchableText (`SearchableText`) COMMENT 'parser "TokenBigramSplitSymbolAlpha"';

* fulltext search score is no longer provided as (column_name) but now provided as (column_name)__score__.

* (category)_title, like source_title, related keys are automatically generated. (category)_description keys as well.
parent 315d57fb
<key_list>
<key>accounting_transaction_line_node_uid | stock/node_uid/z_related_accounting_transaction_stock_line</key>
<key>accounting_transaction_line_total_price | stock/total_price/z_related_accounting_transaction_stock_line</key>
<key>accounting_transaction_mirror_section_title | catalog/title/z_related_accounting_transaction_mirror_section</key>
<key>accounting_transaction_payment_title | catalog/title/z_related_accounting_transaction_payment</key>
<key>accounting_transaction_project_title | catalog/title/z_related_accounting_transaction_project</key>
<key>accounting_transaction_section_title | catalog/title/z_related_accounting_transaction_section</key>
<key>accounting_transaction_mirror_section_title | catalog_full_text/title/z_related_accounting_transaction_mirror_section</key>
<key>accounting_transaction_payment_title | catalog_full_text/title/z_related_accounting_transaction_payment</key>
<key>accounting_transaction_project_title | catalog_full_text/title/z_related_accounting_transaction_project</key>
<key>accounting_transaction_section_title | catalog_full_text/title/z_related_accounting_transaction_section</key>
<key>preferred_gap_id | category,catalog/id/z_related_preferred_gap</key>
<key>preferred_gap_strict_membership_id | category,catalog/id/z_related_strict_membership_preferred_gap</key>
</key_list>
\ No newline at end of file
preferred_gap_id | category,catalog/id/z_related_preferred_gap
preferred_gap_strict_membership_id | category,catalog/id/z_related_strict_membership_preferred_gap
accounting_transaction_mirror_section_title | catalog/title/z_related_accounting_transaction_mirror_section
accounting_transaction_section_title | catalog/title/z_related_accounting_transaction_section
accounting_transaction_project_title | catalog/title/z_related_accounting_transaction_project
accounting_transaction_payment_title | catalog/title/z_related_accounting_transaction_payment
accounting_transaction_mirror_section_title | catalog_full_text/title/z_related_accounting_transaction_mirror_section
accounting_transaction_section_title | catalog_full_text/title/z_related_accounting_transaction_section
accounting_transaction_project_title | catalog_full_text/title/z_related_accounting_transaction_project
accounting_transaction_payment_title | catalog_full_text/title/z_related_accounting_transaction_payment
accounting_transaction_line_node_uid | stock/node_uid/z_related_accounting_transaction_stock_line
accounting_transaction_line_total_price | stock/total_price/z_related_accounting_transaction_stock_line
\ No newline at end of file
......@@ -53,6 +53,7 @@
<value> <string encoding="cdata"><![CDATA[
from DateTime import DateTime\n
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery\n
\n
# params\n
section_title = \'My Organisation\'\n
......@@ -90,7 +91,7 @@ if 1:\n
def getAccountByTitle(title):\n
account_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Account\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(account_list) == 1, \\\n
\'%d account with title "%s"\' % (len(account_list), title)\n
return account_list[0]\n
......@@ -98,7 +99,7 @@ def getAccountByTitle(title):\n
def getOrganisationByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Organisation\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d organisation with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
......@@ -109,14 +110,14 @@ euro_resource = \'currency_module/euro\'\n
def getBankAccountByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Bank Account\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d Bank Account with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
\n
product_list = [o.getObject() for o in portal.portal_catalog(\n
portal_type=\'Product\',\n
title=\'Dummy Product for testing\')]\n
title=SimpleQuery(title=\'Dummy Product for testing\', comparison_operator=\'=\'))]\n
if product_list:\n
product = product_list[0]\n
else:\n
......
......@@ -53,6 +53,7 @@
<value> <string encoding="cdata"><![CDATA[
from DateTime import DateTime\n
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery\n
\n
# params\n
section_title = \'My Organisation\'\n
......@@ -80,7 +81,7 @@ if 1:\n
def getAccountByTitle(title):\n
account_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Account\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(account_list) == 1, \\\n
\'%d account with title "%s"\' % (len(account_list), title)\n
return account_list[0]\n
......@@ -88,7 +89,7 @@ def getAccountByTitle(title):\n
def getOrganisationByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Organisation\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d organisation with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
......@@ -106,7 +107,7 @@ euro_resource = getCurrencyByReference(\'EUR\')\n
def getBankAccountByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Bank Account\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d Bank Account with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
......
......@@ -58,6 +58,7 @@ business_process = \'business_process_module/erp5_default_business_process\'\n
portal = context.getPortalObject()\n
accounting_module = portal.accounting_module\n
from DateTime import DateTime\n
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery\n
year = 2005\n
\n
# if the previous test didn\'t change input data, no need to recreate content\n
......@@ -82,7 +83,7 @@ if 1:\n
def getAccountByTitle(title):\n
account_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Account\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(account_list) == 1, \\\n
\'%d account with title "%s"\' % (len(account_list), title)\n
return account_list[0]\n
......@@ -90,7 +91,7 @@ def getAccountByTitle(title):\n
def getOrganisationByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Organisation\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d organisation with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
......@@ -99,7 +100,7 @@ section = getOrganisationByTitle(section_title)\n
def getPersonByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Person\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d person with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
......@@ -116,7 +117,7 @@ euro_resource = getCurrencyByReference(\'EUR\')\n
def getBankAccountByTitle(title):\n
document_list = [x.getObject().getRelativeUrl() for x in\n
portal.portal_catalog(portal_type=\'Bank Account\',\n
title=title)]\n
title=SimpleQuery(title=title, comparison_operator=\'=\'))]\n
assert len(document_list) == 1, \\\n
\'%d Bank Account with title "%s"\' % (len(document_list), title)\n
return document_list[0]\n
......
......@@ -51,6 +51,7 @@
<item>
<key> <string>_body</string> </key>
<value> <string>portal = context.getPortalObject()\n
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery\n
\n
# validate rules\n
for rule in portal.portal_rules.objectValues():\n
......@@ -129,7 +130,7 @@ stool.setSelectionColumns(\'account_module_selection\',\n
\n
# delete the "dummy account" we create in test_account_gap_parallel_list_field\n
dummy_account_list = portal.account_module.searchFolder(\n
title=\'Dummy Account for UI Test\')\n
title=SimpleQuery(title=\'Dummy Account for UI Test\', comparison_operator=\'=\'))\n
if dummy_account_list:\n
portal.account_module.manage_delObjects([dummy_account_list[0].getId()])\n
\n
......
......@@ -2,8 +2,8 @@
<key>child_address_SearchableText | catalog,full_text/SearchableText/z_related_child_address</key>
<key>child_telephone_SearchableText | catalog,full_text/SearchableText/z_related_child_telephone</key>
<key>default_email_text | catalog,email/url_string/z_related_default_email</key>
<key>destination_person_title | category,catalog/title/z_related_destination_person</key>
<key>destination_person_title | category,catalog_full_text/title/z_related_destination_person</key>
<key>related_resource_use_uid | category,category,catalog,catalog/uid/z_related_resource_use</key>
<key>source_organisation_title | category,catalog/title/z_related_source_organisation</key>
<key>source_person_title | category,catalog/title/z_related_source_person</key>
<key>source_organisation_title | category,catalog_full_text/title/z_related_source_organisation</key>
<key>source_person_title | category,catalog_full_text/title/z_related_source_person</key>
</key_list>
\ No newline at end of file
source_organisation_title | category,catalog/title/z_related_source_organisation
source_person_title | category,catalog/title/z_related_source_person
destination_person_title | category,catalog/title/z_related_destination_person
source_organisation_title | category,catalog_full_text/title/z_related_source_organisation
source_person_title | category,catalog_full_text/title/z_related_source_person
destination_person_title | category,catalog_full_text/title/z_related_destination_person
default_email_text | catalog,email/url_string/z_related_default_email
related_resource_use_uid | category,category,catalog,catalog/uid/z_related_resource_use
child_address_SearchableText | catalog,full_text/SearchableText/z_related_child_address
......
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_core_proxy_field_legacy
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_workflow
\ No newline at end of file
erp5_core_proxy_field_legacy
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_workflow
\ No newline at end of file
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_core_proxy_field_legacy
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_workflow
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string>from Products.ZSQLCatalog.SQLCatalog import Query, SimpleQuery, AndQuery\n
\n
########### CONFIGURATION ######################################\n
# This is a query for search translated properties of Person #\n
# documents. If you want to get other results. Customise this. # \n
################################################################\n
\n
query = AndQuery(SimpleQuery(**{\'content_translation.translated_text\': value, \'comparison_operator\': \'match\'}),\n
Query(**{\'content_translation.property_name\': \'title\'})\n
)\n
\n
################################################################\n
# Above query must make SQL condition like this. #\n
################################################################\n
# MATCH(content_translation.translated_text) AGAINST({value})\n
# AND content_translation.property_name = \'title\'\n
\n
return query\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>value</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SQLCatalog_makeContentTranslationSearchQuery</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<key_list>
<key>content_translation_title | SQLCatalog_makeContentTranslationSearchQuery</key>
<key>content_translation_title | SQLCatalog_makeTranslatedTitleQuery</key>
</key_list>
\ No newline at end of file
content_translation_title | SQLCatalog_makeContentTranslationSearchQuery
\ No newline at end of file
content_translation_title | SQLCatalog_makeTranslatedTitleQuery
\ No newline at end of file
......@@ -6,5 +6,5 @@
<key>related_source_or_source_decision_or_destination_or_destination_decision | category,catalog,catalog/uid/z_related_source_or_source_decision_or_destination_or_destination_decision</key>
<key>related_source_or_source_section_or_destination_or_destination_section | category,catalog,catalog/uid/z_related_source_or_source_section_or_destination_or_destination_section</key>
<key>related_source_section_or_destination_section | category,catalog,catalog/uid/z_related_source_section_or_destination_section</key>
<key>source_organisation_title | category,catalog/title/z_related_source_organisation</key>
<key>source_organisation_title | category,catalog_full_text/title/z_related_source_organisation</key>
</key_list>
\ No newline at end of file
source_organisation_title | category,catalog/title/z_related_source_organisation
source_organisation_title | category,catalog_full_text/title/z_related_source_organisation
event_causality_ticket_uid | category,catalog,category,catalog/uid/z_related_event_causality_ticket
related_source_or_destination | category,catalog,catalog/uid/z_related_source_or_destination
related_source_section_or_destination_section | category,catalog,catalog/uid/z_related_source_section_or_destination_section
......
......@@ -51,7 +51,7 @@
<item>
<key> <string>_body</string> </key>
<value> <string>return (\'erp5_core_proxy_field_legacy\',\n
\'erp5_full_text_myisam_catalog\',\n
\'erp5_full_text_mroonga_catalog\',\n
\'erp5_base\',\n
\'erp5_workflow\',\n
\'erp5_configurator\',\n
......
......@@ -51,7 +51,7 @@
<item>
<key> <string>_body</string> </key>
<value> <string>return (\'erp5_core_proxy_field_legacy\',\n
\'erp5_full_text_myisam_catalog\',\n
\'erp5_full_text_mroonga_catalog\',\n
\'erp5_base\',\n
\'erp5_simulation\',\n
\'erp5_dhtml_style\',\n
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_web
erp5_ingestion_mysql_innodb_catalog
......
<key_list>
<key>SearchableText</key>
<key>full_text.SearchableText</key>
</key_list>
\ No newline at end of file
......@@ -68,8 +68,9 @@ for path in path_list:\n
try:\n
tmp_dict = {}\n
for property in property_list:\n
if property == \'SearchableText\':\n
value = obj.SearchableText()\n
getter = getattr(obj, property, None)\n
if getter is not None and callable(getter):\n
value = getter()\n
else:\n
value = getattr(obj, \'get%s\' % UpperCase(property))()\n
tmp_dict[property] = value\n
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_col</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z0_drop_catalog_fulltext</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string>DROP TABLE IF EXISTS catalog_full_text</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<catalog_method>
<item key="sql_uncatalog_object" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>uid</string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_deferred_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z0_uncatalog_catalog_fulltext</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
DELETE FROM catalog_full_text WHERE <dtml-sqltest uid op=eq type=int>
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<catalog_method>
<item key="sql_catalog_object_list" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>uid\r\n
getTitle\r\n
getDescription</string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_deferred_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_catalog_catalog_fulltext_list</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
REPLACE INTO\n
catalog_full_text (`uid`, `title`, `description`)\n
VALUES\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
(\n
<dtml-sqlvar expr="uid[loop_item]" type="int">, \n
<dtml-sqlvar expr="getTitle[loop_item]" type="string" optional>,\n
<dtml-sqlvar expr="getDescription[loop_item]" type="string" optional>\n
)<dtml-unless sequence-end>,</dtml-unless>\n
</dtml-in>\n
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -55,21 +55,16 @@ SearchableText</string> </value>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
DELETE FROM\n
full_text\n
WHERE\n
<dtml-in uid>\n
uid=<dtml-sqlvar sequence-item type="int"><dtml-if sequence-end><dtml-else> OR </dtml-if>\n
</dtml-in>\n
;\n
<dtml-var "\'\\0\'"><dtml-let document_list="[]">\n
<dtml-let document_list="[]" delete_list="[]">\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
<dtml-if "SearchableText[loop_item]">\n
<dtml-call expr="document_list.append(loop_item)">\n
<dtml-else>\n
<dtml-call expr="delete_list.append(loop_item)">\n
</dtml-if>\n
</dtml-in>\n
<dtml-if expr="_.len(document_list) > 0">\n
INSERT INTO\n
REPLACE INTO\n
full_text\n
VALUES\n
<dtml-in prefix="loop" expr="document_list">\n
......@@ -79,7 +74,19 @@ VALUES\n
)<dtml-unless sequence-end>,</dtml-unless>\n
</dtml-in>\n
</dtml-if>\n
</dtml-let>
<dtml-if expr="_.len(delete_list) > 0">\n
<dtml-var sql_delimiter>\n
DELETE FROM\n
full_text\n
WHERE uid IN\n
( \n
<dtml-in prefix="loop" expr="delete_list">\n
<dtml-sqlvar expr="uid[loop_item]" type="int"><dtml-unless sequence-end>,</dtml-unless>\n
</dtml-in>\n
)\n
</dtml-if>\n
</dtml-let>\n
]]></string> </value>
</item>
......
<catalog_method>
<item key="sql_clear_catalog" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_col</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_create_catalog_fulltext</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string># Host:\n
# Database: test\n
# Table: \'catalog_full_text\'\n
#\n
CREATE TABLE `catalog_full_text` (\n
`uid` BIGINT UNSIGNED NOT NULL,\n
`title` varchar(255) default \'\',\n
`description` text,\n
PRIMARY KEY (`uid`),\n
FULLTEXT `title` (`title`) COMMENT \'parser "TokenBigramSplitSymbolAlpha"\',\n
FULLTEXT `description` (`description`) COMMENT \'parser "TokenBigramSplitSymbolAlpha"\'\n
) ENGINE=mroonga;\n
</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -64,7 +64,7 @@
`content_language` VARBINARY(100),\n
`translated_text` TEXT,\n
PRIMARY KEY (`uid`, `property_name`, `content_language`),\n
FULLTEXT KEY (`translated_text`)\n
FULLTEXT KEY (`translated_text`) COMMENT \'parser "TokenBigramSplitSymbolAlpha"\'\n
) ENGINE=mroonga;\n
</string> </value>
</item>
......
......@@ -66,7 +66,7 @@ CREATE TABLE `full_text` (\n
`uid` BIGINT UNSIGNED NOT NULL,\n
`SearchableText` MEDIUMTEXT,\n
PRIMARY KEY (`uid`),\n
FULLTEXT `SearchableText` (`SearchableText`)\n
FULLTEXT `SearchableText` (`SearchableText`) COMMENT \'parser "TokenBigramSplitSymbolAlpha"\'\n
) ENGINE=mroonga;\n
</string> </value>
</item>
......
<key_list>
<key>career_skill_title | category,catalog,catalog_full_text/title/z_related_career_skill</key>
<key>description | catalog_full_text/description/z_related_uid</key>
<key>parent_description | catalog_full_text/description/z_related_parent</key>
<key>parent_title | catalog_full_text/title/z_related_parent</key>
<key>stock_explanation_title | catalog_full_text/title/z_related_explanation_from_stock</key>
<key>stock_mirror_section_title | catalog_full_text/title/z_related_mirror_section_uid_from_stock</key>
<key>stock_node_title | catalog_full_text/title/z_related_node_uid_from_stock</key>
<key>title | catalog_full_text/title/z_related_uid</key>
</key_list>
\ No newline at end of file
<key_list>
<key>catalog_full_text</key>
<key>full_text</key>
</key_list>
\ No newline at end of file
<key_list>
<key>SearchableText | MroongaBooleanFullTextKey</key>
<key>catalog_full_text.description | MroongaBooleanFullTextKey</key>
<key>catalog_full_text.title | MroongaBooleanFullTextKey</key>
<key>description | MroongaBooleanFullTextKey</key>
<key>full_text.SearchableText | MroongaBooleanFullTextKey</key>
<key>title | MroongaBooleanFullTextKey</key>
</key_list>
\ No newline at end of file
SearchableText
full_text.SearchableText
\ No newline at end of file
erp5_mysql_innodb/SQLCatalog_deferFullTextIndex
erp5_mysql_innodb/SQLCatalog_deferFullTextIndexActivity
erp5_mysql_innodb/SQLCatalog_makeFullTextQuery
erp5_mysql_innodb/z0_drop_catalog_fulltext
erp5_mysql_innodb/z0_drop_content_translation
erp5_mysql_innodb/z0_drop_fulltext
erp5_mysql_innodb/z0_uncatalog_catalog_fulltext
erp5_mysql_innodb/z0_uncatalog_fulltext
erp5_mysql_innodb/z_catalog_catalog_fulltext_list
erp5_mysql_innodb/z_catalog_fulltext_list
erp5_mysql_innodb/z_create_catalog_fulltext
erp5_mysql_innodb/z_create_content_translation
erp5_mysql_innodb/z_create_fulltext
\ No newline at end of file
career_skill_title | category,catalog,catalog_full_text/title/z_related_career_skill
description | catalog_full_text/description/z_related_uid
parent_description | catalog_full_text/description/z_related_parent
parent_title | catalog_full_text/title/z_related_parent
stock_explanation_title | catalog_full_text/title/z_related_explanation_from_stock
stock_mirror_section_title | catalog_full_text/title/z_related_mirror_section_uid_from_stock
stock_node_title | catalog_full_text/title/z_related_node_uid_from_stock
title | catalog_full_text/title/z_related_uid
\ No newline at end of file
SearchableText | MroongaBooleanFullTextKey
catalog_full_text.description | MroongaBooleanFullTextKey
catalog_full_text.title | MroongaBooleanFullTextKey
description | MroongaBooleanFullTextKey
full_text.SearchableText | MroongaBooleanFullTextKey
title | MroongaBooleanFullTextKey
\ No newline at end of file
<key_list>
<key>SearchableText</key>
<key>full_text.SearchableText</key>
<key>catalog_full_text.description</key>
<key>catalog_full_text.title</key> |
<key>description</key> |
<key>full_text.SearchableText</key> |
<key>title</key> |
</key_list>
\ No newline at end of file
......@@ -68,8 +68,9 @@ for path in path_list:\n
try:\n
tmp_dict = {}\n
for property in property_list:\n
if property == \'SearchableText\':\n
value = obj.SearchableText()\n
getter = getattr(obj, property, None)\n
if getter is not None and callable(getter):\n
value = getter()\n
else:\n
value = getattr(obj, \'get%s\' % UpperCase(property))()\n
tmp_dict[property] = value\n
......
<catalog_method>
<item key="sql_clear_catalog" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_col</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z0_drop_catalog_fulltext</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string>DROP TABLE IF EXISTS catalog_full_text</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<catalog_method>
<item key="sql_uncatalog_object" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>uid</string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_deferred_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z0_uncatalog_catalog_fulltext</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
DELETE FROM catalog_full_text WHERE <dtml-sqltest uid op=eq type=int>
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<catalog_method>
<item key="sql_catalog_object_list" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>uid\r\n
getTitle\r\n
getDescription</string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_deferred_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_catalog_catalog_fulltext_list</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
REPLACE INTO\n
catalog_full_text (`uid`, `title`, `description`)\n
VALUES\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
(\n
<dtml-sqlvar expr="uid[loop_item]" type="int">, \n
<dtml-sqlvar expr="getTitle[loop_item]" type="string" optional>,\n
<dtml-sqlvar expr="getDescription[loop_item]" type="string" optional>\n
)<dtml-unless sequence-end>,</dtml-unless>\n
</dtml-in>\n
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -55,21 +55,16 @@ SearchableText</string> </value>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
DELETE FROM\n
full_text\n
WHERE\n
<dtml-in uid>\n
uid=<dtml-sqlvar sequence-item type="int"><dtml-if sequence-end><dtml-else> OR </dtml-if>\n
</dtml-in>\n
;\n
<dtml-var "\'\\0\'"><dtml-let document_list="[]">\n
<dtml-let document_list="[]" delete_list="[]">\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
<dtml-if "SearchableText[loop_item]">\n
<dtml-call expr="document_list.append(loop_item)">\n
<dtml-else>\n
<dtml-call expr="delete_list.append(loop_item)">\n
</dtml-if>\n
</dtml-in>\n
<dtml-if expr="_.len(document_list) > 0">\n
INSERT INTO\n
REPLACE INTO\n
full_text\n
VALUES\n
<dtml-in prefix="loop" expr="document_list">\n
......@@ -79,7 +74,19 @@ VALUES\n
)<dtml-unless sequence-end>,</dtml-unless>\n
</dtml-in>\n
</dtml-if>\n
</dtml-let>
<dtml-if expr="_.len(delete_list) > 0">\n
<dtml-var sql_delimiter>\n
DELETE FROM\n
full_text\n
WHERE uid IN\n
( \n
<dtml-in prefix="loop" expr="delete_list">\n
<dtml-sqlvar expr="uid[loop_item]" type="int"><dtml-unless sequence-end>,</dtml-unless>\n
</dtml-in>\n
)\n
</dtml-if>\n
</dtml-let>\n
]]></string> </value>
</item>
......
<catalog_method>
<item key="sql_clear_catalog" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_col</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>arguments_src</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_create_catalog_fulltext</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1000</int> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string># Host:\n
# Database: test\n
# Table: \'catalog_full_text\'\n
#\n
CREATE TABLE `catalog_full_text` (\n
`uid` BIGINT UNSIGNED NOT NULL,\n
`title` varchar(255) default \'\',\n
`description` text,\n
PRIMARY KEY (`uid`),\n
FULLTEXT `title` (`title`),\n
FULLTEXT `description` (`description`)\n
) ENGINE=MyISAM;\n
</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<key_list>
<key>career_skill_title | category,catalog,catalog_full_text/title/z_related_career_skill</key>
<key>description | catalog_full_text/description/z_related_uid</key>
<key>parent_description | catalog_full_text/description/z_related_parent</key>
<key>parent_title | catalog_full_text/title/z_related_parent</key>
<key>stock_explanation_title | catalog_full_text/title/z_related_explanation_from_stock</key>
<key>stock_mirror_section_title | catalog_full_text/title/z_related_mirror_section_uid_from_stock</key>
<key>stock_node_title | catalog_full_text/title/z_related_node_uid_from_stock</key>
<key>title | catalog_full_text/title/z_related_uid</key>
</key_list>
\ No newline at end of file
<key_list>
<key>catalog_full_text</key>
<key>full_text</key>
</key_list>
\ No newline at end of file
SearchableText
full_text.SearchableText
\ No newline at end of file
catalog_full_text.description
catalog_full_text.title
description
full_text.SearchableText
title
\ No newline at end of file
erp5_mysql_innodb/SQLCatalog_deferFullTextIndex
erp5_mysql_innodb/SQLCatalog_deferFullTextIndexActivity
erp5_mysql_innodb/SQLCatalog_makeFullTextQuery
erp5_mysql_innodb/z0_drop_catalog_fulltext
erp5_mysql_innodb/z0_drop_content_translation
erp5_mysql_innodb/z0_drop_fulltext
erp5_mysql_innodb/z0_uncatalog_catalog_fulltext
erp5_mysql_innodb/z0_uncatalog_fulltext
erp5_mysql_innodb/z_catalog_catalog_fulltext_list
erp5_mysql_innodb/z_catalog_fulltext_list
erp5_mysql_innodb/z_create_catalog_fulltext
erp5_mysql_innodb/z_create_content_translation
erp5_mysql_innodb/z_create_fulltext
\ No newline at end of file
career_skill_title | category,catalog,catalog_full_text/title/z_related_career_skill
description | catalog_full_text/description/z_related_uid
parent_description | catalog_full_text/description/z_related_parent
parent_title | catalog_full_text/title/z_related_parent
stock_explanation_title | catalog_full_text/title/z_related_explanation_from_stock
stock_mirror_section_title | catalog_full_text/title/z_related_mirror_section_uid_from_stock
stock_node_title | catalog_full_text/title/z_related_node_uid_from_stock
title | catalog_full_text/title/z_related_uid
\ No newline at end of file
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_ingestion_mysql_innodb_catalog
erp5_web
erp5_crm
......
<key_list>
<key>item_catalog_portal_type | catalog/portal_type/z_related_item_catalog</key>
<key>item_catalog_reference | catalog/reference/z_related_item_catalog</key>
<key>item_catalog_title | catalog/title/z_related_item_catalog</key>
<key>item_catalog_title | catalog_full_text/title/z_related_item_catalog</key>
<key>item_catalog_validation_state | catalog/validation_state/z_related_item_catalog</key>
</key_list>
\ No newline at end of file
item_catalog_title | catalog/title/z_related_item_catalog
item_catalog_title | catalog_full_text/title/z_related_item_catalog
item_catalog_portal_type | catalog/portal_type/z_related_item_catalog
item_catalog_reference | catalog/reference/z_related_item_catalog
item_catalog_validation_state | catalog/validation_state/z_related_item_catalog
\ No newline at end of file
......@@ -96,7 +96,7 @@
<tr>\n
<td>type</td>\n
<td>search_text</td>\n
<td>Pouet_NO_EXISTS</td>\n
<td>Poueet</td>\n
</tr>\n
<tr>\n
<td>clickAndWait</td>\n
......@@ -111,7 +111,7 @@
<tr>\n
<td>verifyValue</td>\n
<td>search_text</td>\n
<td>Pouet_NO_EXISTS</td>\n
<td>Poueet</td>\n
</tr>\n
<tr>\n
<td>type</td>\n
......
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_web
erp5_ingestion
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_simulation
erp5_configurator_standard_trade_template
erp5_simulation_test
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_ingestion_mysql_innodb_catalog
erp5_ingestion
\ No newline at end of file
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
......@@ -26,6 +26,7 @@
##############################################################################
from Products.ERP5.Document.ERP5ProjectUnitTestDistributor import ERP5ProjectUnitTestDistributor
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery
from zLOG import LOG,ERROR
from AccessControl import ClassSecurityInfo
......@@ -110,7 +111,10 @@ class ERP5ScalabilityDistributor(ERP5ProjectUnitTestDistributor):
tag = "%s_%s" % (self.getRelativeUrl(), title)
if portal.portal_activities.countMessageWithTag(tag) == 0:
test_node_list = test_node_module.searchFolder(portal_type="Test Node",title=title)
test_node_list = test_node_module.searchFolder(
portal_type="Test Node",
title=SimpleQuery(comparison_operator='=', title=title),
)
assert len(test_node_list) in (0, 1), "Unable to find testnode : %s" % title
test_node = None
if len(test_node_list) == 1:
......@@ -129,7 +133,8 @@ class ERP5ScalabilityDistributor(ERP5ProjectUnitTestDistributor):
isMasterTestnode : return True if the node given in parameter exists and is a validated master
"""
test_node_module = self._getTestNodeModule()
test_node_master = [ node for node in test_node_module.searchFolder(portal_type="Test Node", title=title,
test_node_master = [ node for node in test_node_module.searchFolder(portal_type="Test Node",
title=SimpleQuery(comparison_operator='=', title=title),
specialise_uid=self.getUid(),
validation_state="validated") if node.getMaster() == 1 ]
if len(test_node_master) == 1:
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
\ No newline at end of file
......@@ -41,7 +41,7 @@ class TestOxatisSynchronization(ERP5TypeTestCase):
""" Return the list of BT required by unit tests. """
return (
'erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_pdm',
'erp5_simulation',
......
......@@ -102,7 +102,7 @@
<tr>\n
<td>assertValue</td>\n
<td>listbox_title</td>\n
<td>"%1%" OR "%2%"</td>\n
<td>("%1%" OR "%2%")</td>\n
</tr>\n
<tr>\n
<td>clickAndWait</td>\n
......@@ -119,7 +119,7 @@
<tr>\n
<td>assertValue</td>\n
<td>listbox_title</td>\n
<td>"%1%" OR "%2%"</td>\n
<td>("%1%" OR "%2%")</td>\n
</tr>\n
\n
<tr>\n
......@@ -137,7 +137,7 @@
<tr>\n
<td>assertValue</td>\n
<td>listbox_title</td>\n
<td>"%1%" OR "%2%"</td>\n
<td>("%1%" OR "%2%")</td>\n
</tr>\n
\n
<tr>\n
......@@ -181,7 +181,7 @@
<tr>\n
<td>assertValue</td>\n
<td>listbox_title</td>\n
<td>"%1%" OR "%2%"</td>\n
<td>("%1%" OR "%2%")</td>\n
</tr>\n
\n
<tal:block tal:condition="python: context.TestTool_getSkinName()!=\'Mobile\'">\n
......@@ -213,7 +213,7 @@
<tr>\n
<td>assertValue</td>\n
<td>listbox_title</td>\n
<td>"%1%" OR "%2%"</td>\n
<td>("%1%" OR "%2%")</td>\n
</tr>\n
\n
<tr>\n
......
......@@ -78,7 +78,7 @@
<tr>\n
<td>type</td>\n
<td>field_my_foo_category_title</td>\n
<td>b</td>\n
<td>=b</td>\n
</tr>\n
<tr>\n
<td>type</td>\n
......
......@@ -126,7 +126,7 @@ metal:use-macro="here/RelationFieldZuite_CommonTemplate/macros/init"\n
<tr>\n
<td>type</td>\n
<td>listbox_title</td>\n
<td>a</td>\n
<td>=a</td>\n
</tr>\n
<tr>\n
<td>clickAndWait</td>\n
......@@ -135,7 +135,7 @@ metal:use-macro="here/RelationFieldZuite_CommonTemplate/macros/init"\n
</tr>\n
<tr>\n
<td>click</td>\n
<td>//*[@class=\'listbox-data-line-0 DataA\']//input[@type="checkbox"]</td>\n
<td>//tr//a[.="a"]/parent::td/preceding-sibling::td/input[@type="checkbox"]</td>\n
<td></td>\n
</tr>\n
<tr>\n
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_upgrader_test
\ No newline at end of file
......@@ -52,7 +52,7 @@
<key> <string>_body</string> </key>
<value> <string>template_tool = context\n
\n
return template_tool.upgradeSite((\'erp5_full_text_myisam_catalog\',),\n
return template_tool.upgradeSite((\'erp5_full_text_mroonga_catalog\',),\n
dry_run=(not fixit))\n
</string> </value>
</item>
......
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_jquery
erp5_ingestion_mysql_innodb_catalog
......
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_web
erp5_ingestion_mysql_innodb_catalog
......
erp5_ingestion_mysql_innodb_catalog
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
erp5_jquery
erp5_web
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_pdm
erp5_data_set
\ No newline at end of file
erp5_full_text_myisam_catalog
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
......@@ -37,7 +37,7 @@ import string
from zLOG import LOG,INFO,ERROR
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions
from Products.ZSQLCatalog.SQLCatalog import Query
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery
TEST_SUITE_MAX = 4
# Depending on the test suite priority, we will affect
# more or less cores
......@@ -210,7 +210,10 @@ class ERP5ProjectUnitTestDistributor(XMLObject):
tag = "%s_%s" % (self.getRelativeUrl(), title)
if portal.portal_activities.countMessageWithTag(tag) == 0:
test_node_list = test_node_module.searchFolder(portal_type="Test Node",title=title)
test_node_list = test_node_module.searchFolder(
portal_type="Test Node",
title=SimpleQuery(comparison_operator='=', title=title),
)
assert len(test_node_list) in (0, 1), "Unable to find testnode : %s" % title
test_node = None
if len(test_node_list) == 1:
......@@ -244,9 +247,11 @@ class ERP5ProjectUnitTestDistributor(XMLObject):
from_date = now - 30
def getTestSuiteSortKey(test_suite):
test_result = portal.portal_catalog(portal_type="Test Result",
title='="%s"' % test_suite.getTitle(),
modification_date=Query(**{"creation_date": from_date,
"range": "min"}),
title=SimpleQuery(title=test_suite.getTitle()),
creation_date=SimpleQuery(
creation_date=from_date,
comparison_operator='>=',
),
sort_on=[("modification_date", "descending")],
limit=1)
if len(test_result):
......@@ -304,7 +309,10 @@ class ERP5ProjectUnitTestDistributor(XMLObject):
config_list = []
tag = "%s_%s" % (self.getRelativeUrl(), title)
if portal.portal_activities.countMessageWithTag(tag) == 0:
test_node_list = test_node_module.searchFolder(portal_type="Test Node",title=title)
test_node_list = test_node_module.searchFolder(
portal_type="Test Node",
title=SimpleQuery(comparison_operator='=', title=title),
)
assert len(test_node_list) in (0, 1), "Unable to find testnode : %s" % title
test_node = None
if len(test_node_list) == 1:
......@@ -365,7 +373,9 @@ class ERP5ProjectUnitTestDistributor(XMLObject):
def _getTestNodeFromTitle(self, node_title):
test_node_list = self._getTestNodeModule().searchFolder(
portal_type='Test Node', title="='%s'" % node_title)
portal_type="Test Node",
title=SimpleQuery(comparison_operator='=', title=node_title),
)
assert len(test_node_list) == 1, "We found %i test nodes for %s" % (
len(test_node_list), node_title)
test_node = test_node_list[0].getObject()
......@@ -373,7 +383,9 @@ class ERP5ProjectUnitTestDistributor(XMLObject):
def _getTestSuiteFromTitle(self, suite_title):
test_suite_list = self._getTestSuiteModule().searchFolder(
portal_type='Test Suite', title="='%s'" % suit_tile, validation_state="validated")
portal_type='Test Suite',
title=SimpleQuery(comparison_operator='=', title=suite_title),
validation_state='validated')
assert len(test_suite_list) == 1, "We found %i test suite for %s" % (
len(test_suite_list), name)
test_suite = test_suite_list[0].getObject()
......
......@@ -30,6 +30,7 @@ import random
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery
from zLOG import LOG
from xmlrpclib import Binary
......@@ -91,7 +92,7 @@ class TaskDistributionTool(BaseTool):
def createTestResultLineList(test_result, test_name_list):
duration_list = []
previous_test_result_list = portal.test_result_module.searchFolder(
title='="%s"' % test_result.getTitle(),
title=SimpleQuery(comparison_operator='=', title=test_result.getTitle()),
sort_on=[('creation_date','descending')],
simulation_state=('stopped', 'public_stopped'),
limit=1)
......@@ -126,7 +127,7 @@ class TaskDistributionTool(BaseTool):
int_index, reference = revision
result_list = portal.test_result_module.searchFolder(
portal_type="Test Result",
title='="%s"' % test_title,
title=SimpleQuery(comparison_operator='=', title=test_title),
sort_on=(("creation_date","descending"),),
limit=1)
if result_list:
......@@ -162,7 +163,7 @@ class TaskDistributionTool(BaseTool):
test_result._setIntIndex(int_index)
if project_title is not None:
project_list = portal.portal_catalog(portal_type='Project',
title='="%s"' % project_title)
title=SimpleQuery(comparison_operator='=', title=project_title))
if len(project_list) != 1:
raise ValueError('found this list of project : %r for title %r' % \
([x.path for x in project_list], project_title))
......
erp5_full_text_myisam_catalog
erp5_full_text_mroonga_catalog
erp5_base
\ No newline at end of file
......@@ -56,8 +56,12 @@ portal = context.getPortalObject()\n
# This scriptable key supports content_translation if the table is present\n
catalog = portal.portal_catalog.getSQLCatalog()\n
if \'content_translation\' in catalog.getProperty(\'sql_search_tables\'):\n
return AndQuery(SimpleQuery(**{\'content_translation.translated_text\': value, \'comparison_operator\': \'match\'}),\n
Query(**{\'content_translation.property_name\': \'title\'}))\n
if [x for x in catalog.getProperty(\'sql_catalog_search_keys\', []) if \'Mroonga\' in x]:\n
return AndQuery(SimpleQuery(**{\'content_translation.translated_text\': value, \'comparison_operator\': \'mroonga_boolean\'}),\n
Query(**{\'content_translation.property_name\': \'title\'}))\n
else:\n
return AndQuery(SimpleQuery(**{\'content_translation.translated_text\': value, \'comparison_operator\': \'match_boolean\'}),\n
Query(**{\'content_translation.property_name\': \'title\'}))\n
\n
# Otherwise it simply use title\n
return Query(title=value)\n
......
......@@ -82,7 +82,7 @@ class TestCRM(BaseTestCRM):
return "CRM"
def getBusinessTemplateList(self):
return ('erp5_full_text_myisam_catalog',
return ('erp5_full_text_mroonga_catalog',
'erp5_core_proxy_field_legacy',
'erp5_base',
'erp5_ingestion',
......@@ -571,7 +571,7 @@ class TestCRMMailIngestion(BaseTestCRM):
def getBusinessTemplateList(self):
# Mail Ingestion must work with CRM alone.
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_ingestion',
'erp5_ingestion_mysql_innodb_catalog',
......
......@@ -48,7 +48,7 @@ class TestERP5Coordinate(ERP5TypeTestCase):
Return the list of required business templates.
"""
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',)
def beforeTearDown(self):
......
......@@ -55,7 +55,7 @@ class TestERP5Credential(ERP5TypeTestCase):
def getBusinessTemplateList(self):
return (
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_core_proxy_field_legacy',
'erp5_base',
'erp5_jquery',
......
......@@ -63,7 +63,7 @@ class TestEgov(ERP5TypeTestCase):
def getBusinessTemplateList(self):
"""return list of business templates to be installed. """
bt_list = ['erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_web',
'erp5_ingestion_mysql_innodb_catalog',
......
......@@ -56,7 +56,7 @@ class TestZeleniumRunMyDocSample(ERP5TypeFunctionalTestCase):
"""
Return the list of business templates.
"""
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_myisam_catalog',
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_mroonga_catalog',
'erp5_base', 'erp5_ui_test_core','erp5_web', 'erp5_ingestion',
'erp5_accounting',
'erp5_jquery', 'erp5_dms', 'erp5_jquery_ui', 'erp5_web',
......
......@@ -43,21 +43,42 @@ class TestI18NSearch(ERP5TypeTestCase):
portal_type='Person',
first_name='Gabriel',
last_name='Fauré',
description='Quick brown fox jumps over the lazy dog.',
)
person2 = person_module.newContent(
portal_type='Person',
first_name='武者小路',
last_name='実篤'
last_name='実篤',
description='Slow white fox jumps over the diligent dog.',
)
self.tic()
# check if 'é' == 'e' collation works
result = person_module.searchFolder(SearchableText='Faure')
self.assertEqual(len(result), 1)
self.assertEqual(result[0].getPath(), person1.getPath())
result = person_module.searchFolder(title='Faure')
self.assertEqual(len(result), 1)
self.assertEqual(result[0].getPath(), person1.getPath())
# check if a partial string of CJK string matches
result = person_module.searchFolder(SearchableText='武者')
self.assertEqual(len(result), 1)
self.assertEqual(result[0].getPath(), person2.getPath())
result = person_module.searchFolder(title='武者')
self.assertEqual(len(result), 1)
self.assertEqual(result[0].getPath(), person2.getPath())
# check boolean language mode search
result = person_module.searchFolder(SearchableText='+quick +fox +dog')
self.assertEqual(len(result), 1)
self.assertEqual(result[0].getPath(), person1.getPath())
result = person_module.searchFolder(description='+quick +fox +dog')
self.assertEqual(len(result), 1)
self.assertEqual(result[0].getPath(), person1.getPath())
# check fulltext search for automatically generated related keys.
self.assertTrue('MATCH' in self.portal.portal_catalog(destination_title='Faure', src__=1))
def test_suite():
suite = unittest.TestSuite()
......
......@@ -50,7 +50,7 @@ class TestKMMixIn(TestDocumentMixin):
manager_password = ''
website_id = 'km_test'
business_template_list = ['erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog','erp5_base',
'erp5_full_text_mroonga_catalog','erp5_base',
'erp5_jquery', 'erp5_jquery_ui', 'erp5_knowledge_pad',
'erp5_ingestion_mysql_innodb_catalog', 'erp5_ingestion',
'erp5_web', 'erp5_dms',
......
......@@ -68,7 +68,7 @@ class TestSpellChecking(ERP5TypeTestCase):
return "Spell Checking Test"
def getBusinessTemplateList(self):
return ('erp5_full_text_myisam_catalog',
return ('erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_simulation',
'erp5_accounting',
......
......@@ -52,7 +52,7 @@ class TestTemplateTool(ERP5TypeTestCase):
def getBusinessTemplateList(self):
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_stock_cache',
'erp5_csv_style')
......@@ -650,7 +650,7 @@ class TestTemplateTool(ERP5TypeTestCase):
'erp5_core_proxy_field_legacy': first_group,
'erp5_mysql_innodb_catalog': first_group,
'erp5_core': first_group,
'erp5_full_text_myisam_catalog': first_group,
'erp5_full_text_mroonga_catalog': first_group,
'erp5_xhtml_style': first_group,
'erp5_ingestion_mysql_innodb_catalog': second_group,
'erp5_base': second_group,
......
......@@ -881,9 +881,10 @@ class CatalogTool (UniqueObject, ZCatalog, CMFCoreCatalogTool, ActiveObject):
by looking at the category tree.
For exemple it will generate:
destination_title | category,catalog/title/z_related_destination
default_destination_title | category,catalog/title/z_related_destination
strict_destination_title | category,catalog/title/z_related_strict_destination
destination_reference | category,catalog/reference/z_related_destination
default_destination_reference | category,catalog/reference/z_related_destination
strict_destination_reference | category,catalog/reference/z_related_strict_destination
destination_title | category,catalog_full_text/title/z_related_destination
strict_ related keys only returns documents which are strictly member of
the category.
......@@ -917,9 +918,18 @@ class CatalogTool (UniqueObject, ZCatalog, CMFCoreCatalogTool, ActiveObject):
if related:
end_key = end_key[len(related_string):]
# XXX: joining with non-catalog tables is not trivial and requires
# ZSQLCatalog's ColumnMapper cooperation, so only allow catalog
# columns.
if 'catalog' in column_map.get(end_key, ()):
# ZSQLCatalog's ColumnMapper cooperation, so only allow columns in
# catalog or catalog_full_text tables.
if end_key != 'uid' and 'catalog_full_text' in column_map.get(end_key, ()):
related_key_list.append(
prefix + key + ' | category,catalog_full_text/' +
end_key +
'/z_related_' +
('strict_' if strict else '') +
expected_base_cat_id +
('_related' if related else '')
)
elif 'catalog' in column_map.get(end_key, ()):
is_uid = end_key == 'uid'
if is_uid:
end_key = 'uid' if related else 'category_uid'
......
......@@ -49,7 +49,7 @@ class TestArchive(InventoryAPITestCase):
def getBusinessTemplateList(self):
return InventoryAPITestCase.getBusinessTemplateList(self) + (
'erp5_archive',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
)
# Different variables used for this test
......
......@@ -44,7 +44,7 @@ from Products.ERP5Type.tests.utils import createZODBPythonScript, todo_erp5, \
from Products.ZSQLCatalog.ZSQLCatalog import HOT_REINDEXING_FINISHED_STATE,\
HOT_REINDEXING_RECORDING_STATE, HOT_REINDEXING_DOUBLE_INDEXING_STATE
from Products.CMFActivity.Errors import ActivityFlushError
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery, SimpleQuery
from OFS.ObjectManager import ObjectManager
......@@ -92,7 +92,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
return "ERP5Catalog"
def getBusinessTemplateList(self):
return ('erp5_full_text_myisam_catalog', 'erp5_base',)
return ('erp5_full_text_mroonga_catalog', 'erp5_base',)
# Different variables used for this test
username = 'seb'
......@@ -124,6 +124,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
self.getCategoryTool().region,
self.getCategoryTool().group ]:
module.manage_delObjects(list(module.objectIds()))
module.reindexObject()
# Remove copied sql_connector and catalog
if self.new_erp5_sql_connection in self.portal.objectIds():
self.portal.manage_delObjects([self.new_erp5_sql_connection])
......@@ -189,7 +190,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
person = person_module.newContent(id='1',portal_type='Person')
path_list = [person.getRelativeUrl()]
self.checkRelativeUrlNotInSQLPathList(path_list)
person.immediateReindexObject()
self.tic()
self.checkRelativeUrlInSQLPathList(path_list)
person_module.manage_delObjects('1')
self.tic()
......@@ -197,10 +198,10 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
# Now we will ask to immediatly reindex
person = person_module.newContent(id='2',
portal_type='Person',)
person.immediateReindexObject()
self.tic()
path_list = [person.getRelativeUrl()]
self.checkRelativeUrlInSQLPathList(path_list)
person.immediateReindexObject()
self.tic()
self.checkRelativeUrlInSQLPathList(path_list)
person_module.manage_delObjects('2')
self.tic()
......@@ -209,7 +210,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
person = person_module.newContent(id='3',portal_type='Person')
path_list = [person.getRelativeUrl()]
self.checkRelativeUrlNotInSQLPathList(path_list)
person.immediateReindexObject()
self.tic()
self.checkRelativeUrlInSQLPathList(path_list)
person_module.deleteContent('3')
# Now delete things is made with activities
......@@ -223,10 +224,10 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertEqual([],folder_object_list)
person = person_module.newContent(id='4',portal_type='Person',)
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertEqual(['4'],folder_object_list)
person.immediateReindexObject()
self.tic()
person_module.manage_delObjects('4')
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
......@@ -240,7 +241,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
self.assertEqual([],folder_object_list)
person = person_module.newContent(id='4',portal_type='Person')
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertEqual(['4'],folder_object_list)
......@@ -274,7 +275,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
portal_catalog.manage_catalogClear()
person = person_module.newContent(id='4',portal_type='Person')
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertEqual(['4'],folder_object_list)
......@@ -298,7 +299,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
portal_catalog.manage_catalogClear()
person = person_module.newContent(id='4',portal_type='Person')
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertEqual(['4'],folder_object_list)
......@@ -310,11 +311,11 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
portal_catalog.manage_catalogClear()
person = person_module.newContent(id='a',portal_type='Person',title='a',description='z')
person.immediateReindexObject()
self.tic()
person = person_module.newContent(id='b',portal_type='Person',title='a',description='y')
person.immediateReindexObject()
self.tic()
person = person_module.newContent(id='c',portal_type='Person',title='a',description='x')
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId()
for x in person_module.searchFolder(sort_on=[('id','ascending')])]
self.assertEqual(['a','b','c'],folder_object_list)
......@@ -335,11 +336,11 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
portal_catalog.manage_catalogClear()
person = person_module.newContent(id='a',portal_type='Person',title='1')
person.immediateReindexObject()
self.tic()
person = person_module.newContent(id='b',portal_type='Person',title='2')
person.immediateReindexObject()
self.tic()
person = person_module.newContent(id='c',portal_type='Person',title='12')
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getTitle() for x in person_module.searchFolder(sort_on=[('title','ascending')])]
self.assertEqual(['1','12','2'],folder_object_list)
folder_object_list = [x.getObject().getTitle() for x in person_module.searchFolder(sort_on=[('title','ascending','int')])]
......@@ -654,7 +655,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
title = 'Sébastien'
person = person_module.newContent(id='5',portal_type='Person',title=title)
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertEqual(['5'],folder_object_list)
folder_object_list = [x.getObject().getId() for x in
......@@ -666,7 +667,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
title = 'Sébastien'
person = person_module.newContent(id='5',portal_type='Person', title=title)
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in
person_module.searchFolder(title=title)]
self.assertEqual(['5'],folder_object_list)
......@@ -690,10 +691,10 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
portal_category.group.objectIds()])
group_nexedi_category = portal_category.group\
.newContent( id = 'nexedi', title='Nexedi',
description='a')
reference='a')
group_nexedi_category2 = portal_category.group\
.newContent( id = 'storever', title='Storever',
description='b')
reference='b')
module = portal.getDefaultModule('Organisation')
organisation = module.newContent(portal_type='Organisation',)
organisation.setGroup('nexedi')
......@@ -716,17 +717,17 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
organisation_list = [x.getObject() for x in
module.searchFolder(group_id='storever')]
self.assertEqual(organisation_list,[organisation2])
# Try to get the organisation with the group description 'a'
# Try to get the organisation with the group reference 'a'
organisation_list = [x.getObject() for x in
module.searchFolder(group_description='a')]
module.searchFolder(group_reference='a')]
self.assertEqual(organisation_list,[organisation])
# Try to get the organisation with the group description 'c'
# Try to get the organisation with the group reference 'c'
organisation_list = [x.getObject() for x in
module.searchFolder(group_description='c')]
module.searchFolder(group_reference='c')]
self.assertEqual(organisation_list,[])
# Try to get the organisation with the default group description 'c'
# Try to get the organisation with the default group reference 'c'
organisation_list = [x.getObject() for x in
module.searchFolder(default_group_description='c')]
module.searchFolder(default_group_reference='c')]
self.assertEqual(organisation_list,[])
# Try to get the organisation with group relative_url
group_relative_url = group_nexedi_category.getRelativeUrl()
......@@ -752,10 +753,10 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
portal_category.group.objectIds()])
group_nexedi_category = portal_category.group\
.newContent( id = 'nexedi', title='Nexedi',
description='a')
reference='a')
sub_group_nexedi = group_nexedi_category\
.newContent( id = 'erp5', title='ERP5',
description='b')
reference='b')
module = portal.getDefaultModule('Organisation')
organisation = module.newContent(portal_type='Organisation',)
organisation.setGroup('nexedi/erp5')
......@@ -771,13 +772,13 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
organisation_list = [x.getObject() for x in
module.searchFolder(strict_group_title='ERP5')]
self.assertEqual(organisation_list,[organisation])
# Try to get the organisation with the group description a
# Try to get the organisation with the group reference a
organisation_list = [x.getObject() for x in
module.searchFolder(strict_group_description='a')]
module.searchFolder(strict_group_reference='a')]
self.assertEqual(organisation_list,[])
# Try to get the organisation with the group description b
# Try to get the organisation with the group reference b
organisation_list = [x.getObject() for x in
module.searchFolder(strict_group_description='b')]
module.searchFolder(strict_group_reference='b')]
self.assertEqual(organisation_list,[organisation])
def test_22_SearchingWithUnicode(self):
......@@ -793,7 +794,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
# Now we will ask to immediatly reindex
person = person_module.newContent(id='2',
portal_type='Person',)
person.immediateReindexObject()
self.tic()
path_list = [person.getRelativeUrl()]
self.checkRelativeUrlInSQLPathList(path_list)
# We will delete the connector
......@@ -855,19 +856,19 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
"""Sort-on parameter and related key. (Assumes that region_title is a \
valid related key)"""
self.assertTrue(
self.getCatalogTool().buildSQLQuery(region_title='foo',
sort_on=(('region_title', 'ascending'),))['order_by_expression'].endswith('.`title` ASC'))
self.getCatalogTool().buildSQLQuery(region_reference='foo',
sort_on=(('region_reference', 'ascending'),))['order_by_expression'].endswith('.`reference` ASC'))
self.assertTrue(
self.getCatalogTool().buildSQLQuery(region_title='foo',
sort_on=(('region_title', 'descending'),))['order_by_expression'].endswith('.`title` DESC'))
self.getCatalogTool().buildSQLQuery(region_reference='foo',
sort_on=(('region_reference', 'descending'),))['order_by_expression'].endswith('.`reference` DESC'))
self.assertTrue(
self.getCatalogTool().buildSQLQuery(
sort_on=(('region_title', 'ascending'),))['order_by_expression'].endswith('.`title` ASC'),
sort_on=(('region_reference', 'ascending'),))['order_by_expression'].endswith('.`reference` ASC'),
'sort_on parameter must be taken into account even if related key '
'is not a parameter of the current query')
self.assertTrue(
self.getCatalogTool().buildSQLQuery(
sort_on=(('region_title', 'descending'),))['order_by_expression'].endswith('.`title` DESC'),
sort_on=(('region_reference', 'descending'),))['order_by_expression'].endswith('.`reference` DESC'),
'sort_on parameter must be taken into account even if related key '
'is not a parameter of the current query')
......@@ -957,8 +958,8 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
self.assertEqual([organisation.getPath()],
[x.path for x in self.getCatalogTool()(
title={'query': (organisation_title, 'something else'),
'operator': 'or'})])
**{'catalog.title':{'query': (organisation_title, 'something else'),
'operator': 'or'}})])
def test_33_SimpleQueryDictWithAndOperator(self):
"""use a dict as a keyword parameter, with AND operator.
......@@ -1149,20 +1150,19 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
SearchableText='title')[0][0])
# 'different' is found in more than 50% of records
# MySQL ignores such a word, but Tritonn does not ignore.
try:
self.portal.erp5_sql_connection.manage_test('SHOW SENNA STATUS')
except ProgrammingError:
# MySQL ignores such a word, but Mroonga does not ignore.
if 'ENGINE=Mroonga' in self.portal.erp5_sql_connection.manage_test(
'SHOW CREATE TABLE full_text')[0][1]:
# Mroonga
self.assertEqual(10, self.getCatalogTool().countResults(
portal_type='Organisation', SearchableText='different')[0][0])
else:
# MySQL
self.assertEqual([],
[x.getObject for x in self.getCatalogTool()(
portal_type='Organisation', SearchableText='different')])
self.assertEqual(0, self.getCatalogTool().countResults(
portal_type='Organisation', SearchableText='different')[0][0])
else:
# Tritonn
self.assertEqual(10, self.getCatalogTool().countResults(
portal_type='Organisation', SearchableText='different')[0][0])
def test_43_ManagePasteObject(self):
portal_catalog = self.getCatalogTool()
......@@ -1221,11 +1221,11 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
# Recursive Complex Query
# (title='abc' and description='abc') OR
# title='foo' and description='bar'
catalog_kw = {'query':ComplexQuery(ComplexQuery(Query(title='abc'),
Query(description='abc'),
catalog_kw = {'query':ComplexQuery(ComplexQuery(SimpleQuery(title='abc'),
SimpleQuery(description='abc'),
operator='AND'),
ComplexQuery(Query(title='foo'),
Query(description='bar'),
ComplexQuery(SimpleQuery(title='foo'),
SimpleQuery(description='bar'),
operator='AND'),
operator='OR')}
self.failIfDifferentSet([org_a.getPath(), org_f.getPath()],
......@@ -1578,11 +1578,11 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
catalog = portal_catalog.objectValues()[0]
person = person_module.newContent(id='a',portal_type='Person',title='a',description='z')
person.immediateReindexObject()
self.tic()
person = person_module.newContent(id='b',portal_type='Person',title='a',description='y')
person.immediateReindexObject()
self.tic()
person = person_module.newContent(id='c',portal_type='Person',title='a',description='x')
person.immediateReindexObject()
self.tic()
index_columns = getattr(catalog, 'sql_catalog_index_on_order_keys', None)
self.assertNotEqual(index_columns, None)
self.assertEqual(len(index_columns), 0)
......@@ -2105,7 +2105,7 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
# description is not a keyword by default. (This might change in the
# future, in this case, this test have to be updated)
self.assertSameSet([doc], [x.getObject() for x in
ctool(portal_type='Organisation', description='Foo')])
ctool(portal_type='Organisation', description='=Foo')])
self.assertEqual({doc, other_doc}, {x.getObject() for x in
ctool(portal_type='Organisation', description=dict(query='Foo',
key='Keyword'))})
......@@ -3546,19 +3546,19 @@ VALUES
portal_category.group.objectIds()])
group_nexedi_category = portal_category.group\
.newContent( id = 'nexedi', title='Nexedi',
description='a')
reference='a')
group_nexedi_category2 = portal_category.group\
.newContent( id = 'storever', title='Storever',
description='b')
reference='b')
module = portal.getDefaultModule('Organisation')
organisation = module.newContent(portal_type='Organisation',
title='Nexedi Orga',
description='c')
reference='c')
organisation.setGroup('nexedi')
self.assertEqual(organisation.getGroupValue(), group_nexedi_category)
organisation2 = module.newContent(portal_type='Organisation',
title='Storever Orga',
description='d')
reference='d')
organisation2.setGroup('storever')
organisation2.setTitle('Organisation 2')
self.assertEqual(organisation2.getGroupValue(), group_nexedi_category2)
......@@ -3580,19 +3580,19 @@ VALUES
category_list = [x.getObject() for x in
base_category.searchFolder(group_related_id='storever')]
self.assertEqual(category_list,[group_nexedi_category2])
# Try to get the category with the group related organisation description 'd'
# Try to get the category with the group related organisation reference 'd'
category_list = [x.getObject() for x in
base_category.searchFolder(group_related_description='d')]
base_category.searchFolder(group_related_reference='d')]
self.assertEqual(category_list,[group_nexedi_category2])
# Try to get the category with the group related organisation description
# Try to get the category with the group related organisation reference
# 'e'
category_list = [x.getObject() for x in
base_category.searchFolder(group_related_description='e')]
base_category.searchFolder(group_related_reference='e')]
self.assertEqual(category_list,[])
# Try to get the category with the default group related organisation description
# Try to get the category with the default group related organisation reference
# 'e'
category_list = [x.getObject() for x in
base_category.searchFolder(default_group_related_description='e')]
base_category.searchFolder(default_group_related_reference='e')]
self.assertEqual(category_list,[])
# Try to get the category with the group related organisation relative_url
organisation_relative_url = organisation.getRelativeUrl()
......@@ -3618,19 +3618,19 @@ VALUES
portal_category.group.objectIds()])
group_nexedi_category = portal_category.group\
.newContent( id = 'nexedi', title='Nexedi',
description='a')
reference='a')
sub_group_nexedi = group_nexedi_category\
.newContent( id = 'erp5', title='ERP5',
description='b')
reference='b')
module = portal.getDefaultModule('Organisation')
organisation = module.newContent(portal_type='Organisation',
title='ERP5 Orga',
description='c')
reference='c')
organisation.setGroup('nexedi/erp5')
self.assertEqual(organisation.getGroupValue(), sub_group_nexedi)
organisation2 = module.newContent(portal_type='Organisation',
title='Nexedi Orga',
description='d')
reference='d')
organisation2.setGroup('nexedi')
# Flush message queue
self.tic()
......@@ -3649,15 +3649,15 @@ VALUES
base_category.portal_catalog(
strict_group_related_title='ERP5 Orga')]
self.assertEqual(category_list,[sub_group_nexedi])
# Try to get the category with the group related organisation description d
# Try to get the category with the group related organisation reference d
category_list = [x.getObject() for x in
base_category.portal_catalog(
strict_group_related_description='d')]
strict_group_related_reference='d')]
self.assertEqual(category_list,[group_nexedi_category])
# Try to get the category with the group related organisation description c
# Try to get the category with the group related organisation reference c
category_list = [x.getObject() for x in
base_category.portal_catalog(
strict_group_related_description='c')]
strict_group_related_reference='c')]
self.assertEqual(category_list,[sub_group_nexedi])
def test_EscapingLoginInSescurityQuery(self):
......@@ -3768,7 +3768,7 @@ VALUES
title='foo (bar)'
person = person_module.newContent(portal_type='Person',title=title)
person_id = person.getId()
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertTrue(person_id in folder_object_list)
folder_object_list = [x.getObject().getId() for x in
......@@ -3781,15 +3781,16 @@ VALUES
# Make sure that the catalog will not split it with such research :
# title=foo AND title=bar
title='foo bar'
person_module.newContent(portal_type='Person',title=title).immediateReindexObject()
person_module.newContent(portal_type='Person',title=title)
self.tic()
title = title.replace(' ', ' ')
person = person_module.newContent(portal_type='Person',title=title)
person_id = person.getId()
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertTrue(person_id in folder_object_list)
folder_object_list = [x.getObject().getId() for x in
person_module.searchFolder(title=title)]
person_module.searchFolder(**{'catalog.title':title})]
self.assertEqual([person_id],folder_object_list)
def test_SearchFolderWithSingleQuote(self):
......@@ -3800,7 +3801,7 @@ VALUES
title="foo 'bar"
person = person_module.newContent(portal_type='Person',title=title)
person_id = person.getId()
person.immediateReindexObject()
self.tic()
folder_object_list = [x.getObject().getId() for x in person_module.searchFolder()]
self.assertTrue(person_id in folder_object_list)
folder_object_list = [x.getObject().getId() for x in
......@@ -3817,7 +3818,7 @@ VALUES
person = person_module.newContent(portal_type='Person',title=title,
description=description)
person_uid = person.getUid()
person.immediateReindexObject()
self.tic()
folder_object_list = person_module.searchFolder(uid=person_uid, select_dict={'title': None})
new_title = 'bar'
new_description = 'foobarfoo'
......
......@@ -42,7 +42,7 @@ class TestERP5CatalogSecurityUidOptimization(ERP5TypeTestCase):
XXX: Inherit from TestERP5Catalog so we test default and security_uid optmization with same tests.
"""
business_template_list = ['erp5_security_uid_innodb_catalog',
'erp5_full_text_myisam_catalog','erp5_base']
'erp5_full_text_mroonga_catalog','erp5_base']
def getBusinessTemplateList(self):
return self.business_template_list
......
......@@ -85,7 +85,7 @@ class TestLiveConfiguratorWorkflowMixin(SecurityTestCase):
def getBusinessTemplateList(self):
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_workflow',
'erp5_configurator',
......
......@@ -43,7 +43,7 @@ class TestConfiguratorItem(TestLiveConfiguratorWorkflowMixin):
def getBusinessTemplateList(self):
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_workflow',
'erp5_configurator',
......
......@@ -38,7 +38,7 @@ class TestConfiguratorTool(TestLiveConfiguratorWorkflowMixin):
def getBusinessTemplateList(self):
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_workflow',
'erp5_configurator',
......
......@@ -47,7 +47,7 @@ class TestZeleniumConfiguratorStandard(ERP5TypeFunctionalTestCase):
"""
Return the list of business templates.
"""
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_myisam_catalog',
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_mroonga_catalog',
'erp5_base', 'erp5_workflow', 'erp5_configurator',
'erp5_configurator_standard', 'erp5_jquery',
'erp5_ui_test_core', 'erp5_accounting',
......
......@@ -1271,7 +1271,8 @@ class SelectionTool( BaseTool, SimpleItem ):
if len(field_value):
sql_catalog = self.portal_catalog.getSQLCatalog()
field_value = sql_catalog.buildQuery({
catalog_index: field_value.splitlines()
catalog_index:{'query':field_value.splitlines(),
'key':'ExactMatch',},
}).asSearchTextExpression(sql_catalog, column='')
REQUEST.form[field_key] = field_value
......
......@@ -103,7 +103,7 @@ class TestDocumentMixin(ERP5TypeTestCase):
business_template_list = ['erp5_core_proxy_field_legacy',
'erp5_jquery',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_ingestion_mysql_innodb_catalog',
'erp5_ingestion',
......@@ -1155,13 +1155,13 @@ class TestDocument(TestDocumentMixin):
self.assertSameSet([document_1,web_page_1], getAdvancedSearchStringResultList(**kw))
# exact word search
kw = {'searchabletext_any': '*',
kw = {'searchabletext_any': '',
'searchabletext_phrase': 'linux python'}
self.assertSameSet([document_1], getAdvancedSearchStringResultList(**kw))
kw = {'searchabletext_any': '*',
kw = {'searchabletext_any': '',
'searchabletext_phrase': 'python linux'}
self.assertSameSet([document_2], getAdvancedSearchStringResultList(**kw))
kw = {'searchabletext_any': '*',
kw = {'searchabletext_any': '',
'searchabletext_phrase': 'python linux knowledge system'}
self.assertSameSet([document_2], getAdvancedSearchStringResultList(**kw))
......@@ -1243,7 +1243,7 @@ class TestDocument(TestDocumentMixin):
# should return all documents matching a word no matter of contributor
self.assertSameSet([web_page_1, document_4], getAdvancedSearchStringResultList(**kw))
kw = {'searchabletext_any': 'owner',
'contributor_title': '%Contributor%'}
'contributor_title': 'Contributor'}
self.assertSameSet([document_4], getAdvancedSearchStringResultList(**kw))
# multiple portal_type specified
......
......@@ -52,7 +52,7 @@ class TestOooDynamicStyle(ERP5TypeTestCase):
def getBusinessTemplateList(self):
return ('erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_ingestion_mysql_innodb_catalog',
'erp5_ingestion',
......
......@@ -49,7 +49,7 @@ class testTioSafeMixin(ERP5TypeTestCase):
""" Return the list of BT required by unit tests. """
return (
'erp5_core_proxy_field_legacy',
'erp5_full_text_myisam_catalog',
'erp5_full_text_mroonga_catalog',
'erp5_base',
'erp5_pdm',
'erp5_simulation',
......
......@@ -41,7 +41,7 @@ class TestAnonymousSelection(TestZeleniumCore):
"""
Return the list of business templates.
"""
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_myisam_catalog',
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_mroonga_catalog',
'erp5_base', 'erp5_ui_test_core', 'erp5_ui_test', 'erp5_forge',
)
......
......@@ -39,7 +39,7 @@ class TestZeleniumCore(ERP5TypeFunctionalTestCase):
"""
Return the list of business templates.
"""
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_myisam_catalog',
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_mroonga_catalog',
'erp5_base', 'erp5_ui_test_core', 'erp5_ui_test', 'erp5_forge',
'erp5_dhtml_style', 'erp5_dhtml_ui_test',
'erp5_jquery', 'erp5_jquery_ui',
......
......@@ -43,7 +43,7 @@ class TestZeleniumKM(ERP5TypeFunctionalTestCase):
"""
# XXX This is a rough list, we should drop as much as we can, and
# keep only minimal
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_myisam_catalog',
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_mroonga_catalog',
'erp5_base', 'erp5_ui_test_core', 'erp5_ui_test', 'erp5_forge',
'erp5_dhtml_style', 'erp5_dhtml_ui_test',
'erp5_jquery', 'erp5_jquery_ui',
......
......@@ -48,7 +48,7 @@ class TestZeleniumStandaloneUserTutorial(ERP5TypeFunctionalTestCase):
"""
Return the list of business templates.
"""
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_myisam_catalog',
return ('erp5_core_proxy_field_legacy', 'erp5_full_text_mroonga_catalog',
'erp5_base', 'erp5_ui_test_core', 'erp5_forge',
'erp5_dhtml_style',
'erp5_jquery', 'erp5_jquery_ui',
......
......@@ -65,7 +65,7 @@ class TestDeferredConnection(ERP5TypeTestCase):
"""
def getBusinessTemplateList(self):
return 'erp5_full_text_myisam_catalog',
return 'erp5_full_text_mroonga_catalog',
def getTitle(self):
return "Deferred Connection"
......
......@@ -33,6 +33,7 @@ from Products.ZSQLCatalog.SQLExpression import SQLExpression
from Products.ZSQLCatalog.interfaces.operator import IOperator
from zope.interface.verify import verifyClass
from Products.ZSQLCatalog.SQLCatalog import list_type_list
import re
class ComparisonOperatorBase(OperatorBase):
def asSQLExpression(self, column, value_list, only_group_columns):
......@@ -105,13 +106,17 @@ class MatchComparisonOperator(MonovaluedComparisonOperator):
This operator can emit a select expression, so it overrides
asSQLExpression inseatd of just defining a render method.
"""
# No need to do full text search for an empty string.
if value_list == '':
column, value_list = self.render(column, value_list)
return SQLExpression(self, where_expression='%s %s %s' % (column, '=', value_list))
match_string = self.where_expression_format_string % {
'column': column,
'value_list': self.renderValue(value_list),
}
select_dict = {}
if not only_group_columns:
select_dict[column.replace('`', '').split('.')[-1]] = match_string
select_dict['%s__score__' % column.replace('`', '').rsplit('.', 1)[-1]] = match_string
# Sort on this column uses relevance.
# TODO: Add a way to allow sorting by raw column value.
order_by_dict = {
......@@ -127,6 +132,50 @@ class MatchComparisonOperator(MonovaluedComparisonOperator):
verifyClass(IOperator, MatchComparisonOperator)
class MroongaComparisonOperator(MatchComparisonOperator):
fulltext_boolean_splitter = re.compile(r'(\s|\(.+?\)|".+?")')
fulltext_boolean_detector = re.compile(r'(^[+-]|^.+\*$|^["(].+[")]$)')
def __init__(self, operator, force_boolean=False):
MatchComparisonOperator.__init__(self, operator, ' IN BOOLEAN MODE')
self.force_boolean = force_boolean
def renderValue(self, value_list):
"""
Special Query renderer for MroongaFullText queries:
* by default 'AND' search by using '*D+' pragma.
* similarity search for non-boolean queries by using '*S"..."' operator.
"""
if isinstance(value_list, list_type_list):
try:
value_list, = value_list
except ValueError:
raise ValueError, '%r: value_list must not contain more than one item. Got %r' % (self, value_list)
if self.force_boolean:
fulltext_query = '*D+ %s' % value_list
return self._renderValue(fulltext_query)
else:
match_query_list = []
match_boolean_query_list = []
for token in self.fulltext_boolean_splitter.split(value_list):
token = token.strip()
if not token:
continue
elif self.fulltext_boolean_detector.match(token):
match_boolean_query_list.append(token)
else:
match_query_list.append(token)
# Always use BOOLEAN MODE to combine similarity search and boolean search.
fulltext_query = '*D+'
if match_query_list:
fulltext_query += ' *S"%s"' % ' '.join(match_query_list)
if match_boolean_query_list:
fulltext_query += ' %s' % ' '.join(match_boolean_query_list)
return self._renderValue(fulltext_query)
verifyClass(IOperator, MroongaComparisonOperator)
class SphinxSEComparisonOperator(MonovaluedComparisonOperator):
def __init__(self, operator, mode=''):
MonovaluedComparisonOperator.__init__(self, operator, '')
......@@ -173,9 +222,10 @@ operator_dict = {
'match': MatchComparisonOperator('match'),
'match_boolean': MatchComparisonOperator('match_boolean', mode=' IN BOOLEAN MODE'),
'match_expansion': MatchComparisonOperator('match_expansion', mode=' WITH QUERY EXPANSION'),
'mroonga': MroongaComparisonOperator('mroonga'),
'mroonga_boolean': MroongaComparisonOperator('mroonga_boolean', force_boolean=True),
'sphinxse': SphinxSEComparisonOperator('sphinxse'),
'in': MultivaluedComparisonOperator('in'),
'is': MonovaluedComparisonOperator('is'),
'is not': MonovaluedComparisonOperator('is not', '!='),
}
......@@ -62,12 +62,14 @@ class RelatedQuery(Query):
self.table_alias_list = table_alias_list
def _asSearchTextExpression(self, sql_catalog, column=None):
assert column is None
assert column in (None, '')
if column is None:
column = self.search_key.getColumn()
join_condition = self.join_condition
if join_condition is None:
result = None
else:
result = join_condition.asSearchTextExpression(sql_catalog, column=self.search_key.getColumn())
result = join_condition.asSearchTextExpression(sql_catalog, column=column)
return False, result
def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
......
......@@ -334,6 +334,9 @@ class SQLExpression(object):
if can_merge_sql_expression and alias in mergeable_set:
# Custom conflict resolution
column = '%s + %s' % (existing_value, column)
elif alias.endswith('__score__'):
# We only support the first full text score in select dict.
pass
else:
message = '%r is a known alias for column %r, can\'t alias it now to column %r' % (alias, existing_value, column)
if DEBUG:
......
......@@ -28,29 +28,30 @@
#
##############################################################################
from SearchKey import SearchKey
from DefaultKey import DefaultKey
from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery
from Products.ZSQLCatalog.SearchText import parse
from Products.ZSQLCatalog.interfaces.search_key import ISearchKey
from Products.ZSQLCatalog.SearchText import dequote
from zope.interface.verify import verifyClass
import re
FULLTEXT_BOOLEAN_DETECTOR = re.compile(r'.*((^|\s)[\+\-<>\(\~]|[\*\)](\s|$))')
class FullTextKey(SearchKey):
class FullTextKey(DefaultKey):
"""
This SearchKey generates SQL fulltext comparisons.
"""
default_comparison_operator = 'match'
get_operator_from_value = False
def parseSearchText(self, value, is_column):
return parse(value, is_column)
def dequoteParsedText(self):
return False
def _renderValueAsSearchText(self, value, operator):
# XXX:
# return value for 'a b' here is '(a b), but keyword search with
# '(a b)' means fulltext search with (a OR b), that is different
# from fulltext='a b' that means fulltext search with (a AND b).
return '(%s)' % (value, )
def _processSearchValue(self, search_value, logical_operator,
......@@ -61,8 +62,8 @@ class FullTextKey(SearchKey):
mode, make the operator for that value be 'match_boolean'.
"""
operator_value_dict, logical_operator, parsed = \
SearchKey._processSearchValue(self, search_value, logical_operator,
comparison_operator)
super(FullTextKey, self)._processSearchValue(
search_value, logical_operator, comparison_operator)
new_value_list = []
append = new_value_list.append
for value in operator_value_dict.pop('match', []):
......@@ -77,6 +78,13 @@ class FullTextKey(SearchKey):
operator_value_dict['match_boolean'].extend(new_value_list)
else:
operator_value_dict['match'] = new_value_list
# Dequote for non full-text queries.
for comparison_operator, value_list in operator_value_dict.iteritems():
if comparison_operator not in ('match', 'match_boolean'):
operator_value_dict[comparison_operator] = [
isinstance(value, basestring) and dequote(value) or value
for value in value_list
]
return operator_value_dict, logical_operator, parsed
def _buildQuery(self, operator_value_dict, logical_operator, parsed, group):
......@@ -88,10 +96,20 @@ class FullTextKey(SearchKey):
column = self.getColumn()
query_list = []
append = query_list.append
for comparison_operator, value_list in operator_value_dict.iteritems():
for comparison_operator in ('match', 'match_boolean'):
value_list = operator_value_dict.pop(comparison_operator, [])
if not value_list:
continue
# XXX:
# In MySQL FTS, no operator implies OR so that we should not merge
# AND queries into one...
append(SimpleQuery(search_key=self,
comparison_operator=comparison_operator,
group=group, **{column: ' '.join(value_list)}))
# Other comparison operators are handled by the super class.
if operator_value_dict:
query_list += super(FullTextKey, self)._buildQuery(
operator_value_dict, logical_operator, parsed, group)
return query_list
verifyClass(ISearchKey, FullTextKey)
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2014 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from MroongaFullTextKey import MroongaFullTextKey
from Products.ZSQLCatalog.interfaces.search_key import ISearchKey
from zope.interface.verify import verifyClass
class MroongaBooleanFullTextKey(MroongaFullTextKey):
"""
This SearchKey generates SQL fulltext comparisons for Mroonga whose
default comparison operator is mroonga_boolean.
"""
default_comparison_operator = 'mroonga_boolean'
verifyClass(ISearchKey, MroongaBooleanFullTextKey)
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2014-2006 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from DefaultKey import DefaultKey
from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery
from Products.ZSQLCatalog.interfaces.search_key import ISearchKey
from Products.ZSQLCatalog.SearchText import dequote
from zope.interface.verify import verifyClass
class MroongaFullTextKey(DefaultKey):
default_comparison_operator = 'mroonga'
def dequoteParsedText(self):
return False
def _renderValueAsSearchText(self, value, operator):
# XXX:
# return value for 'a b' here is '(a b), but keyword search with
# '(a b)' means fulltext search with (a OR b), that is different
# from fulltext='a b' that means fulltext search with (a AND b).
return '(%s)' % (value, )
def _processSearchValue(self, search_value, logical_operator,
comparison_operator):
operator_value_dict, logical_operator, parsed = \
super(MroongaFullTextKey, self)._processSearchValue(
search_value, logical_operator, comparison_operator)
# Dequote for non full-text queries.
for comparison_operator, value_list in operator_value_dict.iteritems():
if comparison_operator not in ('mroonga', 'mroonga_boolean'):
operator_value_dict[comparison_operator] = [
isinstance(value, basestring) and dequote(value) or value
for value in value_list
]
return operator_value_dict, logical_operator, parsed
def _buildQuery(self, operator_value_dict, logical_operator, parsed, group):
"""
Special Query builder for FullText queries: merge all values having the
same operator into just one query, to save SQL server from the burden to
do multiple fulltext lookups when one would suit the purpose.
"""
column = self.getColumn()
query_list = []
append = query_list.append
for comparison_operator in ('mroonga', 'mroonga_boolean'):
value_list = operator_value_dict.pop(comparison_operator, [])
if not value_list:
continue
if logical_operator == 'and':
joined_value = ' '.join(value_list)
append(SimpleQuery(search_key=self,
comparison_operator=comparison_operator,
group=group, **{column:joined_value}))
else:
# TODO : We can join to one query like 'aaa OR (bbb ccc) OR "ddd eee"'.
for value in value_list:
append(SimpleQuery(search_key=self,
comparison_operator=comparison_operator,
group=group, **{column:value}))
# Other comparison operators are handled by the super class.
if operator_value_dict:
query_list += super(MroongaFullTextKey, self)._buildQuery(
operator_value_dict, logical_operator, parsed, group)
return query_list
verifyClass(ISearchKey, MroongaFullTextKey)
......@@ -168,8 +168,10 @@ class DummyCatalog(SQLCatalog):
sql_catalog_keyword_search_keys = ('keyword', )
sql_catalog_datetime_search_keys = ('date', )
sql_catalog_full_text_search_keys = ('fulltext', )
sql_catalog_full_text_search_keys = ('old_fulltext', )
sql_catalog_scriptable_keys = ('scriptable_keyword | scriptableKeyScript', )
sql_catalog_search_keys = ('fulltext | MroongaFullTextKey',
'fulltext_boolean | MroongaBooleanFullTextKey',)
def getColumnMap(self):
"""
......@@ -180,7 +182,9 @@ class DummyCatalog(SQLCatalog):
'default': ['foo', ],
'keyword': ['foo', ],
'date': ['foo', ],
'old_fulltext': ['foo', ],
'fulltext': ['foo', ],
'fulltext_boolean': ['foo', ],
'other_uid': ['bar', ],
'ambiguous_mapping': ['foo', 'bar'],
}
......@@ -495,8 +499,8 @@ class TestSQLCatalog(ERP5TypeTestCase):
# This example introduces impossible-to-merge search text criterion, which
# is allowed as long as
reference_query = ReferenceQuery(
ReferenceQuery(ReferenceQuery(operator='match', fulltext='a'),
ReferenceQuery(ReferenceQuery(operator='match', fulltext='b'),
ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='a'),
ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='b'),
operator='not'), operator='and'), operator='and')
self.catalog(reference_query, {'fulltext': 'a NOT b'})
# The same, with an order by, must raise
......@@ -504,15 +508,15 @@ class TestSQLCatalog(ERP5TypeTestCase):
{'fulltext': 'a NOT b', 'order_by_list': [('fulltext', ), ]},
check_search_text=False)
# If one want to sort on, he must use the equivalent FullText syntax:
self.catalog(ReferenceQuery(ReferenceQuery(operator='match_boolean',
self.catalog(ReferenceQuery(ReferenceQuery(operator='mroonga',
fulltext=MatchList(['a -b', '-b a'])), operator='and'),
{'fulltext': 'a -b', 'order_by_list': [('fulltext', ), ]},
check_search_text=False)
self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a'),
ReferenceQuery(ReferenceQuery(operator='match', fulltext='b'), operator='not'), operator='or'), operator='and'),
self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='a'),
ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='b'), operator='not'), operator='or'), operator='and'),
{'fulltext': 'a OR NOT b'})
self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a'),
ReferenceQuery(ReferenceQuery(operator='match', fulltext='b'), operator='not'), operator='and'), operator='and'),
self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='a'),
ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='b'), operator='not'), operator='and'), operator='and'),
{'fulltext': 'a AND NOT b'})
def test_006_testRelatedKey_with_multiple_join(self):
......@@ -540,7 +544,7 @@ class TestSQLCatalog(ERP5TypeTestCase):
check_search_text=False)
def test_009_testFullTextKey(self):
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a'), operator='and'),
self.catalog(ReferenceQuery(ReferenceQuery(operator='mroonga', fulltext='a'), operator='and'),
{'fulltext': 'a'})
def test_isAdvancedSearchText(self):
......@@ -551,15 +555,20 @@ class TestSQLCatalog(ERP5TypeTestCase):
def test_FullTextSearchMergesQueries(self):
"""
XXX this test is for old FullTextKey, not for MroongaFullTextKey
that merges queries only when logical_operator is 'and'. Also
_renderValueAsSearchText it not perfect so that we cannot use the
test codes below for mroonga search key.
FullText criterion on the same scope must be merged into one query.
Logical operator is ignored, as fulltext operators are expected instead.
"""
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a b'), operator='and'),
{'fulltext': 'a AND b'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a b'), operator='and'),
{'fulltext': 'a OR b'})
self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a b'), operator='not'), operator='and'),
{'fulltext': 'NOT (a b)'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', old_fulltext='a b'), operator='and'),
{'old_fulltext': 'a AND b'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', old_fulltext='a b'), operator='and'),
{'old_fulltext': 'a OR b'})
self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='match', old_fulltext='a b'), operator='not'), operator='and'),
{'old_fulltext': 'NOT (a b)'})
def test_NoneValueToSimpleQuery(self):
"""
......@@ -583,60 +592,69 @@ class TestSQLCatalog(ERP5TypeTestCase):
def test_FullTextBooleanMode(self):
"""
XXX this test is for old FullTextKey, not for MroongaFullTextKey
that does no automatic mode switch.
Fulltext searches must switch automatically to boolean mode if boolean
operators are found in search value.
"""
self.catalog(ReferenceQuery(ReferenceQuery(operator='match_boolean',
fulltext=MatchList(['a*'])), operator='and'),
{'fulltext': 'a*'})
old_fulltext=MatchList(['a*'])), operator='and'),
{'old_fulltext': 'a*'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match_boolean',
fulltext=MatchList(['a* b'])), operator='and'),
{'fulltext': 'a* b'})
old_fulltext=MatchList(['a* b'])), operator='and'),
{'old_fulltext': 'a* b'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', fulltext='*a'),
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', old_fulltext='*a'),
operator='and'),
{'fulltext': '*a'})
{'old_fulltext': '*a'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a'),
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', old_fulltext='a'),
operator='and'),
{'fulltext': 'a'})
{'old_fulltext': 'a'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', fulltext='a+b'), operator='and'),
{'fulltext': 'a+b'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match', old_fulltext='a+b'), operator='and'),
{'old_fulltext': 'a+b'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match_boolean',
fulltext=MatchList(['a +b', '+b a'])), operator='and'),
{'fulltext': 'a +b'}, check_search_text=False)
old_fulltext=MatchList(['a +b', '+b a'])), operator='and'),
{'old_fulltext': 'a +b'}, check_search_text=False)
self.catalog(ReferenceQuery(ReferenceQuery(
ReferenceQuery(operator='=', uid='foo'),
ReferenceQuery(operator='match_boolean',
fulltext=MatchList(['+a b', 'b +a'])),
operator='and'), operator='and'), {'fulltext': '+a b uid:foo'})
old_fulltext=MatchList(['+a b', 'b +a'])),
operator='and'), operator='and'), {'old_fulltext': '+a b uid:foo'})
def test_FullTextQuoting(self):
"""
XXX this test is for old FullTextKey, not for MroongaFullTextKey
that merges queries only when logical_operator is 'and'. Also
_renderValueAsSearchText it not perfect so that we cannot use the
test codes below for mroonga search key.
"""
# Quotes must be kept
self.catalog(ReferenceQuery(ReferenceQuery(operator='match',
fulltext='"a"'), operator='and'),
{'fulltext': '"a"'})
old_fulltext='"a"'), operator='and'),
{'old_fulltext': '"a"'})
self.catalog(ReferenceQuery(ReferenceQuery(operator='match',
fulltext='"foo" bar "baz"'), operator='and'),
{'fulltext': '"foo" bar "baz"'})
old_fulltext='"foo" bar "baz"'), operator='and'),
{'old_fulltext': '"foo" bar "baz"'})
# ...But each column must follow rules defined in configured SearchKey for
# that column (in this case: quotes must be stripped).
ref_query = ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='match',
fulltext='"foo" bar'), ReferenceQuery(operator='=',
old_fulltext='"foo" bar'), ReferenceQuery(operator='=',
default='hoge \"pon'), operator='and'), operator='and')
self.catalog(ref_query, {
'keyword': 'default:"hoge \\"pon" AND fulltext:("foo" bar)'})
'keyword': 'default:"hoge \\"pon" AND old_fulltext:("foo" AND bar)'})
self.catalog(ref_query, {
'fulltext': '"foo" bar AND default:"hoge \\"pon"'})
'old_fulltext': '"foo" bar AND default:"hoge \\"pon"'})
ref_query = ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='match',
fulltext='"\\"foo\\" bar"'), ReferenceQuery(operator='=',
old_fulltext='"\\"foo\\" bar"'), ReferenceQuery(operator='=',
default='hoge \"pon'), operator='and'), operator='and')
self.catalog(ref_query, {
'keyword': 'default:"hoge \\"pon" AND fulltext:"\\"foo\\" bar"'})
'keyword': 'default:"hoge \\"pon" AND old_fulltext:"\\"foo\\" bar"'})
def test_DefaultKeyTextRendering(self):
self.catalog(ReferenceQuery(ReferenceQuery(operator='like', default='a% b'), operator='and'),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment