8

After reading here for ages, I've finally registered to ask a question. I've been messing around with IndexedDB lately and stumbled over a problem with compound indexes (I use them somilar to the example here).

I have an object in the objectstore with a string value, and a couple of integer values. E. g.:

[description:text, value1:int, value2:int, value3:int]

I created a compound index on this object like this:

("compoundIndex", ["value1" , "value2" , "value3"] , { unique: false });

In the html I got a couple of select boxes and a textfield, that allows the user to search for specific entries. The integers are passed as a keyrange to the opencursor-function on the index. Then I use indexOf(textfield) on the resulting set (like done here)

If the selectbox has a value, that value is used as upper and lower bound. If the select box is untouched, the lower range is 1 and the upper is a MAX_INT variable I declared (like described here).

sample code:

transaction = db.transaction(["schaden"] , "readonly").objectStore("schaden");
index = transaction.index("compoundIndex");

// keyrange-arrays from another function    
lowerBound = [valueOneLower, valueTwoLower, valueThreeLower];
upperBound = [valueOneUpper, valueTwoUpper, valueThreeUpper];
range = IDBKeyRange.bound( lowerBound, upperBound );

index.openCursor(range).onsuccess = function(e){
  var cursor = e.target.result;
  if (cursor){
    if (getTextfield.length == 0){
      console.log("Entry found: " + cursor.value.description + ". Object: " + JSON.stringify(cursor.value));
    }else if (cursor.value.bezeichnung.indexOf(getTextfield) !== -1){
      console.log("Entry found: " + cursor.value.description + ". Object: " + JSON.stringify(cursor.value));
    };
    cursor['continue']();                           
    };
  };    

I can search for entries perfectly well, when I have all values set in all the select-boxes. However, if I leave a field open, it messes up the search. Let's say I have not touched the value1-select box, and set the other boxes to 2, I'll get the lowerBound = [1,2,2] and the upperBound = [4294967295,2,2]. This will give me back all entries in my IDB, it doesn't take the 2nd and 3rd value into account.

Is this intended? Or is there a way around this? I have been searching for information about this over and over but seem to be in a dead end. My naive understanding of this API led me to believe it would take all array fields into account on the search. Since the object and therefor also the index I use are much more complex than the example above, performing searches on multiple indexes would be quite messy.

Thanks for your insights!

Edit: To make it a little more clear after the first comments. Let's say if have the following object in the object store:

obj1 { val1 = 1 , val2 = 3 , val3 = 1 }
obj2 { val1 = 1 , val2 = 2 , val3 = 2 }
obj3 { val1 = 2 , val2 = 1 , val3 = 3 }
obj4 { val1 = 1 , val2 = 1 , val3 = 1 }
obj5 { val1 = 1 , val2 = 2 , val3 = 3 }

The index sorts it the way expected:

#1 [1,1,1] obj4
#2 [1,2,2] obj2
#3 [1,2,3] obj5
#4 [1,3,1] obj1
#5 [2,1,3] obj3

Let's assume now I search for the range (lower[1,1,1] , upper[1,1,1]) I'll get obj4. This is the behaviour when all select boxes have selected option 1. Now if I search for an entry with val1 = 1, val2 = unknown and val3 = 1, I get the following range: lower[1,1,1] , upper[1,4294967295,1]. Expected results are obj4 [1,1,1] and obj1 [1,3,1]. Instead of these, the result is giving me 4 hits, namely obj4, obj2, obj5 and obj1 although val3 of obj2 and obj5 doesn't match the key range.

steve-o-mat
  • 81
  • 1
  • 5
  • 2
    Thanks for coming by. We get a lot of new users to SO on the IDB tag. If you've really been "reading here for ages," now that you're registered I encourage you to go back and upvote some of the answers you've been finding helpful. It will reward some of us folks trying to make sure StackOverflow is a good place to ask IndexedDB questions. – buley May 22 '14 at 15:18
  • 2
    @buley: Sure thing. I'll revisit the bookmarks I set. :) Edit: Oh, just figured I need a reputation of 15 to upvote... – steve-o-mat May 23 '14 at 06:53

2 Answers2

6
  1. When you create an index on an array, the entries of your store only appear in the index if each element in the array that corresponds to a property in the underlying object has a defined value.

    To get around this obstacle, always store defined values in your underlying object store. For example, to represent a boolean property, use an integer, where 0 is false, and 1 is true. This way, each object in the store can appear in the index. indexedDB's behavior here is quite different than truthy/falsy handling in plain old javascript (where 0 == undefined ).

  2. The key range you specify when opening a cursor on a array-based index must use defined parameters for each element of the array.

    To get around this obstacle, you must specify all boundaries, even if those boundaries are not real values (e.g. like in my example you linked to, 200 as max age works because we can safely assume no one is 200 yrs old).

So to address your question, it might be a problem in your code in that one of the parameters to your boundaries variables (either [valueOneLower, valueTwoLower, valueThreeLower] or [valueOneUpper, valueTwoUpper, valueThreeUpper]) is not defined.

Based on your comments, I suggest that you test your expectations with indexedDB.cmp. It is pretty simple to write these tests. It does not require any database connection. Here is a pretty basic example to get you started:

// Build our test values

var lower1 = 1, lower2 = 1, lower3 = 1;
var upper1 = 3, upper3 = 3, upper3 = 3;
var middle1 = 2, middle2 = 2, middle3 = 2;

var lowerBound = [lower1,lower2,lower3];
var upperBound = [upper1,upper2,upper3];
var middleValue = [middle1,middle2,middle3];

// As the linked page provides, cmp returns -1 if first is less than second, 0 if equal, 1 if first is greater than second.

var lowerVsMiddle = indexedDB.cmp(lowerBound, middleValue);
console.log('Is %s < %s ? %s', lowerBound, middleValue, lowerVsMiddle == -1);
console.log('Is %s > %s ? %s', lowerBound, middleValue, lowerVsMiddle == 1);

var upperVsMiddle = indexedDB.cmp(upperBound, middleValue);
console.log('Is %s < %s ? %s', upperBound, middleValue, upperVsMiddle == -1);
console.log('Is %s > %s ? %s', upperBound, middleValue, upperVsMiddle == 1);

You should be able to answer your questions accurately by running tests like this.

I retrieved the relevant part of the indexedDB spec for you. First note that "An Array is only a valid key if every item in the array is defined and is a valid key...". This ties into whether the object will appear in the index, and also ties into whether your key parameters to either cmp or IDBKeyRange.bound/lowerBound/upperBound will work. Second, farther down, note the following:

Values of type Array are compared to other values of type Array as follows:

  1. Let A be the first Array value and B be the second Array value.
  2. Let length be the lesser of A's length and B's length.
  3. Let i be 0.
  4. If the ith value of A is less than the ith value of B, then A is less than B. Skip the remaining steps.
  5. If the ith value of A is greater than the ith value of B, then A is greater than B. Skip the remaining steps.
  6. Increase i by 1.
  7. If i is not equal to length, go back to step 4. Otherwise continue to next step.
  8. If A's length is less than B's length, then A is less than B. If A's length is greater than B's length, then A is greater than B. Otherwise A and B are equal.

From the KeyRange section: A key is in a key range if both the following conditions are fulfilled:

  • The key range lower value is undefined or less than key. It may also be equal to key if lowerOpen is false.
  • The key range upper value is undefined or greater than key. It may also be equal to key if upperOpen is false.

One more clarification now that I understand your question based on the comments and your further edits: Essentially indexedDB is providing a union behavior of the criteria but you want is an intersection. One way of solving this is to not think about the data in normal form at all, but to think about how to setup the data so it can be queried in the manner you want. It is interesting food for thought and I do not have an immediate answer for you.

Community
  • 1
  • 1
Josh
  • 17,834
  • 7
  • 50
  • 68
  • 1
    To 1.: Atm I have multiple objects that have a sort of relation. Something like ObjectStore Car {id,brand}, Bike {id,brand} and Person {id,name,car,bike} as example. Car now would have {id:1 brand:Ford, id:2 brand:BMW, ...}. Same goes for bike. In the main object Person I'd only store the ints representing the string in the other objects, e.g. {name:Steve, car:1, bike:2}. In my app I use different objects, but they have to have defined definded cross-references (unlike in this example, in which a person does not necessarily have a car or bike). So I can rule this one out. – steve-o-mat May 23 '14 at 06:10
  • 1
    To 2.: Checked that one already. I always havethe boundaries defined. To stay with the above example, let's say I have a selectbox to pick a car. It reads the objectstore Car and lists the strings as options to pick. The selectbox will return an integer representing the ID if something is selected. If nothing is selected (because you want to search for all entries), I'll get a NaN. I'll transform that to a range then (lower=1, upper=MAX_INT). In contrast, the range when an entry is picked is (lower=pickedID, upper=pickedID). I have tested that heavily, I always get a proper range back. – steve-o-mat May 23 '14 at 06:22
  • The problem now is, that when I do not select the first selectbox (and therefor get a range of 1 to MAX_INT), but the second and the third, it will give me all entries, because the search seems to perform only on the first value in the array. It doesn't throw out those entries not in range of the 2nd and 3rd value. – steve-o-mat May 23 '14 at 06:51
  • Hmm. First, clarify ensure you are not using the multi-entry flag when creating the index, that makes it difficult to answer your question here. – Josh May 23 '14 at 13:51
  • Second, ensure the indices contain what you expect. Examine the contents of the indices using dev-tools. Look at the keypath columns. Look at how a particular index is sorted. – Josh May 23 '14 at 13:52
  • Third, write some simple tests that use indexedDB.cmp. Testing with cmp is a great learning tool. One test should look at two values where one is lower bound and one is value that should be above it. Another test should look at two values where one is upper bound and one is value that should be below it. Maybe I'll edit my answer to help you write these tests. – Josh May 23 '14 at 13:54
  • 1
    Thanks for the help, Josh. The index is not multi-entry and the index lists the objects just fine. Thanks for pointing me towards cmp. I have adapted your code to check my arrays and it turned out just fine. For clarification (I hope), I have added an edit to the original answer. Looks like Kyaw is right and the behaviour I expected simply isn't possible. – steve-o-mat May 26 '14 at 10:41
  • Finally got what you were asking. There are a couple ways I see. One, if you can guarantee order ahead of time, you can arrange the elements of the keypath from narrowest to broadest, ensuring you clamp at the right moment. Two, use multiple compound indices (the exploding index issue), one index for each of the various sets of checkboxes that could be checked (1 all checked, 2 first unchecked, 3 second unchecked, 4 third unchecked, 5 first and second unchecked, 6 first and third unchecked, 7 and second and third unchecked). So 7 indices. One other idea is to try and use a sentinels approach. – Josh May 27 '14 at 01:22
  • Again, thank you Josh. Looks like I really have to run multiple indexes then. Too bad, since like already mentioned this will make things overly complex. Guess I'll also run into the exploding index problem sooner or later. What a bummer. If this would work as expected, it would make searching on IDBs much more convenient. – steve-o-mat May 27 '14 at 07:54
  • @Josh possible to search the compound index in this manner? first key is less-than, second key is greater-than? – Dee Feb 18 '22 at 16:22
1

You need another compound index for that case.

Alternatively you can use key joining as describe here YDN-DB - Incorrect results using mixed data types with SortedMerge

Community
  • 1
  • 1
Kyaw Tun
  • 12,447
  • 10
  • 56
  • 83
  • 1
    Thanks for commenting. So I'd have to go for 2 more compound indexes in that case, right? E.g. [val2, val3, val1] and [val3, val1, val2]. I fail to see the difference between this and having a a simple index on each attribute though. The problem I have with this solution is that my planned object will have 15 to 20 attributes. That would leave me with a lot of indexes and an unneccessary high complexity. Atm I don't really understand why we are able to have compound indexes, when they are not helping to search for multiple conditions in one transaction (with key range that is). – steve-o-mat May 22 '14 at 14:19
  • Btw I have already looked into your library, looks pretty cool. I just wanted to tackle this without any additional wrappers for the beginning to get used to the stock behaviour. Seems I have to give it a go then. Keep up the good work! – steve-o-mat May 22 '14 at 14:21
  • Compound index can have only one range query. When one of the filter is off (un-selected checkbox), it become another range query. Compound index query will not work with two range query. That is why. Yes, compound can lead to exploding index problem https://developers.google.com/appengine/docs/python/datastore/indexes#Python_Index_limits the solution is using zigzag merge. you really need a library to do that. – Kyaw Tun May 24 '14 at 06:48