Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2f56826
Initial not indexed value support (MissingValue, EmptyValue)
andbag May 5, 2019
29b0bf3
Reorganize NotIndexedValue classes
andbag May 6, 2019
ce61b42
Fix BTree TypeError
andbag May 6, 2019
05c8079
Add not indexed value tests
andbag May 6, 2019
2de46a3
Fix get_object_datum for not indexed value support
andbag May 7, 2019
c61afae
Add additional tests for not indexed value support
andbag May 7, 2019
a47ebc7
Fix EmptyValue support
andbag May 7, 2019
d140ffb
Disable Missing/EmptyValue interface of CompositeIndex
andbag May 8, 2019
7f246be
Fix flake8
andbag May 8, 2019
a0c8546
Merge branch 'master' into notindexed_value_support
May 9, 2019
0f6a2ce
Merge branch 'master' into notindexed_value_support
May 10, 2019
7c9cf99
Merge branch 'master' into notindexed_value_support
May 14, 2019
a2a1ee7
New naming of methods and variables
andbag May 10, 2019
6ceb6dc
Store special values in `_unindex`
andbag May 10, 2019
7b9313c
Fix KeywordIndex tests
andbag May 10, 2019
0911a0c
Fix unindex and refactor KeywordIndex
andbag May 13, 2019
47d5b95
Return False for SpecialValues on truth value testing
andbag May 14, 2019
e305aca
Fix flake8
andbag May 14, 2019
8e47c81
Merge branch 'master' into notindexed_value_support
May 17, 2019
b529bd2
Raise Error if multiple indexed attributes are set
andbag May 22, 2019
6b7b84d
Prepare index_object for multiple indexed attributes
andbag May 23, 2019
52114da
Merge branch 'fix_indexed_attr' into notindexed_value_support
andbag May 24, 2019
b494b75
Add test for multiple indexed attributes
andbag May 24, 2019
e319a05
Replace __nonzero__ with __bool__ for py3
andbag May 24, 2019
bcbd74d
Store keywords always as OOset
andbag May 24, 2019
5f709ac
Fix tests for OOset
andbag May 24, 2019
5f6c287
Create debug entry if datum for attribute cannot be determined
andbag May 24, 2019
0c5493a
Fix consistent check for missing `_unindex` entry
andbag May 24, 2019
090bc9e
Remove obsolete code
andbag May 24, 2019
e7c5e07
flake8
andbag May 24, 2019
cf950e6
Fix clearing of special values of KexwordIndex
andbag May 26, 2019
78efffb
Continue to fix clearing of special values
andbag May 27, 2019
7dd55b7
Add definition map for special values
andbag May 27, 2019
0d58199
Refinement of special value support
andbag May 28, 2019
6d5fa3e
Fix test for empty value
andbag May 29, 2019
4110f65
Log then ignore keys not indexable
andbag Jun 4, 2019
2815927
Consolidate code
andbag Jun 4, 2019
2a45691
Avoid obsolete type casting
andbag Jun 4, 2019
6c5e52c
Consolidate special value handling step one
andbag Jun 4, 2019
fd0a9d2
Consolidate special value handling step two
andbag Jun 6, 2019
225df14
Consolidate code of CompositeIndex
andbag Jun 6, 2019
9bbfdfb
Completion of interfaces
andbag Jun 6, 2019
906a858
Code further generalized
andbag Jun 6, 2019
ab99560
flake8
andbag Jun 6, 2019
e595088
Continue cleaning
andbag Jun 6, 2019
d059521
Code further generalized II
andbag Jun 11, 2019
9883352
Reorganize code and complete tests
andbag Jun 11, 2019
62321b9
Fix for py2 backward compatibility
andbag Jun 11, 2019
3913a78
Merge branch 'master' into notindexed_value_support
Jun 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions src/Products/PluginIndexes/CompositeIndex/CompositeIndex.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,23 @@
from BTrees.OOBTree import difference
from BTrees.OOBTree import OOSet
from Persistence import PersistentMapping
from zope.interface import implementer

from Products.PluginIndexes.interfaces import ITransposeQuery
from zope.interface import implementer_only

from Products.PluginIndexes.interfaces import (
ILimitedResultIndex,
IQueryIndex,
ISortIndex,
IUniqueValueIndex,
IRequestCacheIndex,
ITransposeQuery,
MissingValue,
EmptyValue,
)
from Products.PluginIndexes.KeywordIndex.KeywordIndex import KeywordIndex
from Products.PluginIndexes.unindex import _marker
from Products.ZCatalog.query import IndexQuery


LOG = logging.getLogger('CompositeIndex')

QUERY_OPTIONS = {
Expand Down Expand Up @@ -172,7 +182,8 @@ def __repr__(self):
'attributes: {0.attributes}>').format(self)


@implementer(ITransposeQuery)
@implementer_only(ILimitedResultIndex, IQueryIndex, IUniqueValueIndex,
ISortIndex, IRequestCacheIndex, ITransposeQuery)
class CompositeIndex(KeywordIndex):

"""Index for composition of simple fields.
Expand Down Expand Up @@ -380,6 +391,14 @@ def make_query(self, query):
if c.meta_type == 'BooleanIndex':
rec.keys = [int(bool(v)) for v in rec.keys[:]]

# cannot currently support KeywordIndex's
# MissigValue/EmptyValue feature
if c.meta_type == 'KeywordIndex':
if MissingValue in rec.keys:
continue
if EmptyValue in rec.keys:
continue

# rec with 'not' parameter
not_parm = rec.get('not', None)
if not_parm:
Expand Down
62 changes: 49 additions & 13 deletions src/Products/PluginIndexes/KeywordIndex/KeywordIndex.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@
from BTrees.OOBTree import difference
from BTrees.OOBTree import OOSet
from App.special_dtml import DTMLFile
from zope.interface import implementer

from Products.PluginIndexes.unindex import UnIndex
from Products.PluginIndexes.util import safe_callable

from Products.PluginIndexes.interfaces import (
NotIndexedValue,
IIndexingMissingValue,
MissingValue,
IIndexingEmptyValue,
EmptyValue,
)

_marker = []
LOG = getLogger('Zope.KeywordIndex')

try:
Expand All @@ -29,6 +38,7 @@
basestring = (bytes, str)


@implementer(IIndexingMissingValue, IIndexingEmptyValue)
class KeywordIndex(UnIndex):
"""Like an UnIndex only it indexes sequences of items.

Expand Down Expand Up @@ -58,24 +68,28 @@ def _index_object(self, documentId, obj, threshold=None, attr=''):
# we'll do so.

newKeywords = self._get_object_keywords(obj, attr)

oldKeywords = self._unindex.get(documentId, None)

if oldKeywords is None:
# we've got a new document, let's not futz around.
if isinstance(newKeywords, NotIndexedValue):
return self.insertNotIndexed(newKeywords, documentId)

try:
for kw in newKeywords:
self.insertForwardIndexEntry(kw, documentId)
if newKeywords:
self._unindex[documentId] = list(newKeywords)
self._unindex[documentId] = list(newKeywords)
except TypeError:
return 0
else:
# we have an existing entry for this document, and we need
# to figure out if any of the keywords have actually changed
if type(oldKeywords) is not OOSet:
oldKeywords = OOSet(oldKeywords)
newKeywords = OOSet(newKeywords)
if isinstance(newKeywords, NotIndexedValue):
newKeywords = OOSet()
else:
newKeywords = OOSet(newKeywords)
fdiff = difference(oldKeywords, newKeywords)
rdiff = difference(newKeywords, oldKeywords)
if fdiff or rdiff:
Expand All @@ -84,22 +98,32 @@ def _index_object(self, documentId, obj, threshold=None, attr=''):
self._unindex[documentId] = list(newKeywords)
else:
del self._unindex[documentId]

if fdiff:
self.unindex_objectKeywords(documentId, fdiff)
if rdiff:
for kw in rdiff:
self.insertForwardIndexEntry(kw, documentId)

# maybe a previous attribute was a not indexable value
if newKeywords:
self.removeNotIndexed(EmptyValue, documentId)

return 1

def _get_object_keywords(self, obj, attr):
newKeywords = getattr(obj, attr, ())
newKeywords = getattr(obj, attr, None)
if safe_callable(newKeywords):
try:
newKeywords = newKeywords()
except (AttributeError, TypeError):
return ()
return MissingValue

if newKeywords is None:
return MissingValue

if not newKeywords:
return ()
return EmptyValue
elif isinstance(newKeywords, basestring):
return (newKeywords,)
else:
Expand All @@ -122,13 +146,25 @@ def unindex_objectKeywords(self, documentId, keywords):
def unindex_object(self, documentId):
""" carefully unindex the object with integer id 'documentId'"""

keywords = self._unindex.get(documentId, None)
keywords = self._unindex.get(documentId, _marker)

# Couldn't we return 'None' immediately
# if keywords is 'None' (or _marker)???
if keywords is _marker:
res = 0
res += self.removeNotIndexed(MissingValue, documentId)
res += self.removeNotIndexed(EmptyValue, documentId)

if keywords is not None:
self._increment_counter()
if res:
self._increment_counter()
else:
LOG.debug('%(context)s: Attempt to unindex nonexistent '
'document with id %(doc_id)s', dict(
context=self.__class__.__name__,
doc_id=documentId),
exc_info=True)

return None

self._increment_counter()

self.unindex_objectKeywords(documentId, keywords)
try:
Expand Down
50 changes: 40 additions & 10 deletions src/Products/PluginIndexes/KeywordIndex/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
from OFS.SimpleItem import SimpleItem
from Testing.makerequest import makerequest

from Products.PluginIndexes.interfaces import (
MissingValue,
EmptyValue,
)


class Dummy(object):

Expand Down Expand Up @@ -67,20 +72,30 @@ def setUp(self):
(5, Dummy(['a', 'b', 'c', 'e'])),
(6, Dummy(['a', 'b', 'c', 'e', 'f'])),
(7, Dummy(['0'])),
(8, Dummy([])),
(9, Dummy(None)),
]
self._noop_req = {'bar': 123}
self._all_req = {'foo': ['a']}
self._some_req = {'foo': ['e']}
self._overlap_req = {'foo': ['c', 'e']}
self._string_req = {'foo': 'a'}
self._zero_req = {'foo': ['0']}
self._miss_req_1 = {'foo': [MissingValue]}
self._miss_req_2 = {'foo': ['a', MissingValue]}
self._empty_req_1 = {'foo': [EmptyValue]}
self._empty_req_2 = {'foo': [EmptyValue, 'f']}

self._not_1 = {'foo': {'query': 'f', 'not': 'f'}}
self._not_2 = {'foo': {'query': ['e', 'f'], 'not': 'f'}}
self._not_3 = {'foo': {'not': '0'}}
self._not_4 = {'foo': {'not': ['0', 'e']}}
self._not_5 = {'foo': {'not': ['0', 'no-value']}}
self._not_6 = {'foo': 'c', 'bar': {'query': 123, 'not': 1}}
self._not_7 = {'foo': {'not': [MissingValue]}}
self._not_8 = {'foo': {'not': []}}
self._not_9 = {'foo': {'not': [EmptyValue, ]}}
self._not_10 = {'foo': {'not': [EmptyValue, 'f']}}

def _populateIndex(self):
for k, v in self._values:
Expand Down Expand Up @@ -152,38 +167,53 @@ def testEmpty(self):
self._checkApply(self._some_req, [])
self._checkApply(self._overlap_req, [])
self._checkApply(self._string_req, [])
self._checkApply(self._miss_req_1, [])
self._checkApply(self._miss_req_2, [])

def testPopulated(self):
self._populateIndex()
values = self._values

assert len(self._index.referencedObjects()) == len(values)
self.assertEqual(len(self._index.referencedObjects()), len(values) - 2)
assert self._index.getEntryForObject(1234) is None
assert (self._index.getEntryForObject(1234, self._marker)
is self._marker)
self._index.unindex_object(1234) # nothrow
self.assertEqual(self._index.indexSize(), len(values) - 1)
self.assertEqual(self._index.indexSize(), len(values) - 3)

for k, v in values:
entry = self._index.getEntryForObject(k)
for k, v in values[:8]:
entry = self._index.getEntryForObject(k, None)
entry.sort()
kw = sorted(set(v.foo()))
self.assertEqual(entry, kw)

assert len(list(self._index.uniqueValues('foo'))) == len(values) - 1
empty_indexed = self._index.getNotIndexed(EmptyValue)
self.assertEqual(8 in empty_indexed, True)

miss_indexed = self._index.getNotIndexed(MissingValue)
self.assertEqual(9 in miss_indexed, True)

assert len(list(self._index.uniqueValues('foo'))) == len(values) - 3
assert self._index._apply_index(self._noop_req) is None

self._checkApply(self._all_req, values[:-1])
self._checkApply(self._all_req, values[:-3])
self._checkApply(self._some_req, values[5:7])
self._checkApply(self._overlap_req, values[2:7])
self._checkApply(self._string_req, values[:-1])
self._checkApply(self._string_req, values[:-3])
self._checkApply(self._miss_req_1, values[9:10])
self._checkApply(self._miss_req_2, values[:7] + values[9:10])
self._checkApply(self._empty_req_1, values[8:9])
self._checkApply(self._empty_req_2, values[6:7] + values[8:9])

self._checkApply(self._not_1, [])
self._checkApply(self._not_2, values[5:6])
self._checkApply(self._not_3, values[:7])
self._checkApply(self._not_4, values[:5])
self._checkApply(self._not_5, values[:7])
self._checkApply(self._not_3, values[:7] + values[9:10])
self._checkApply(self._not_4, values[:5] + values[9:10])
self._checkApply(self._not_5, values[:7] + values[9:10])
self._checkApply(self._not_6, values[2:7])
self._checkApply(self._not_7, values[:8])
self._checkApply(self._not_8, values[:8] + values[9:10])
self._checkApply(self._not_9, values[:8] + values[9:10])

def testReindexChange(self):
self._populateIndex()
Expand Down
34 changes: 34 additions & 0 deletions src/Products/PluginIndexes/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,37 @@ def make_query(query):

def getIndexNames():
""" returns index names that are optimized by index """


class NotIndexedValue(object):
"""generic marker class for not indexed values"""

def __nonzero__(self):
return 0


class IIndexingMissingValue(Interface):
"""Marker interface to mark indexes with support the
`MissingValue` query term."""


class MissingValue(NotIndexedValue):
"""MissingValue can be used as query "term" to query
for objects the index does not have a value for
(like a not defined value)"""


MissingValue = MissingValue()


class IIndexingEmptyValue(Interface):
"""Marker interface to mark indexes with support the
`EmptyValue` query term."""


class EmptyValue(NotIndexedValue):
"""EmptyValue can be used as query "term" to query
for objects with an empty value (like an empty set)"""


EmptyValue = EmptyValue()
Loading