How do I create a usable index for an array of objects using pouchdb-find?

Viewed 503

I only want to get data with groups that match at least with one of those groupIDs. I have tried a few solutions but I still did it wrong. Assuming we have data like this.

const groupIDs = ['124', '245', '678', '999', '111'];

const data = [
  {
    _id: 'example:123',
    _rev: 'example',
    name: 'John',
    groups: [
      { groupID: '124', type: 'ADMIN' },
      { groupID: '345', type: 'PRACTITIONER' },
      { groupID: '678', type: 'PATIENT' },
    ]
  },
  {
    _id: 'example:456',
    _rev: 'example2',
    name: 'Yoda',
    groups: [
      { groupID: '124', type: 'ADMIN' },
      { groupID: '345', type: 'PRACTITIONER' }
    ]
  },
];

This is my attempt:

db.createIndex({
  index: { fields: ['groups.[].groupID'] }
})
.then((result) => {
  db.find({
    selector: { groups.groupID: { $in: groupIDs } },
    use_index: results.name
  })
})
1 Answers

Here's one of surely many ways to achieve the desired result with Mango. I've also included an example Map/Reduce.

For Mango, the highlights are creating the index and selector. The index goes like so

 db.createIndex({
    index: {
       fields: ['groups'],
       ddoc: 'my-index'
    }
 });

Note I prefer to name the index (ddoc).

So we need combination operators that work with array fields, of which are the $in and $elemMatch operators.

From the CouchDB find documentation[1]:

The $elemMatch operator matches and returns all documents that contain an array field with at least one element matching the supplied query criteria.

Further, the $in documentation states

The document field must exist in the list provided.

Great! Let's use the $elemMatch and $in combination operators to find a set of groupIDs in the groups array.

 selector: {
    groups: {
      $elemMatch: {
         groupID: { $in: groupIDs }
      }
   }

The following snippet demonstrates both a Mango query using the above selector and a Map/Reduce solution using the keys query option.

The significance between the two is that the Map/Reduce produces a document for every match, meaning there may be redundant documents; the Mango result set returns a document if it matches one or more times.

// convenience
const gel = id => document.getElementById(id);

const all_docs = 'allDocs';
const groups_groupID = 'groupID';
const groups_groupIDView = 'groupIDView';
const g_view_result = 'view_result';
const g_groupIdHints = 'groupIdHints';

const queryFns = {};
// show all the documents used in this example.
queryFns[all_docs] = () => db.allDocs({
  include_docs: true
});
// Mango - find one or more groupIDs in the groups array.
// @groupIDs is an array of one or more values.
queryFns[groups_groupID] = (groupIDs /*array*/ ) => {
  const query = {
    selector: {
      groups: {
        $elemMatch: {
          groupID: {
            $in: groupIDs
          }
        }
      }
    },
    use_index: "my-index"
  }
  return db.find(query);
}

// Map/Reduce - query the groupIDView for one or more groupIDs
// @groupIDs is an array of one or more values.
queryFns[groups_groupIDView] = (groupIDs /*array*/ ) => db.query(
  groups_groupIDView, {
    include_docs: true,
    reduce: false,
    keys: groupIDs
  });

// execute a query named in queryFns
const query = async(fnName, csv) => {
  const html = [];
  const view_result = gel(g_view_result);

  try {
    // convert csv to array
    const groupIds = (csv || '').split(',').map(id => id.trim());
    let docs = await queryFns[fnName](groupIds);

    // Ensure docs is an array of documents (e.g. db.allDocs vs db.find result)
    // Beware the SO 'Tidy' function smashes the null coalesce operator 
    docs = docs.rows ?? docs.docs;

    html.push(`Matches: ${docs.length || 0}`);
    // collect the docs for view
    docs.forEach(doc =>
      html.push(`<pre>${JSON.stringify(doc,undefined,3)}</pre>`)
    );

  } catch (e) {
    // display any error message
    html.unshift(e.message);
  }

  view_result.innerHTML = html.join('<hr/>');
}

// canned demo documents
function getDocsToInstall() {
  return [{
      _id: 'example:123',
      //_rev: 'example',
      name: 'John',
      groups: [{
          groupID: '124',
          type: 'ADMIN'
        },
        {
          groupID: '345',
          type: 'PRACTITIONER'
        },
        {
          groupID: '678',
          type: 'PATIENT'
        },
      ]
    },
    {
      _id: 'example:456',
      // _rev: 'example2',
      name: 'Yoda',
      groups: [{
          groupID: '124',
          type: 'ADMIN'
        },
        {
          groupID: '345',
          type: 'PRACTITIONER'
        }
      ]
    },
  ]
}

//
// init db 
//
let db;

(async() => {

  db = new PouchDB('test', {
    adapter: 'memory'
  });

  // install the docs into the db
  const docs = getDocsToInstall();
  await db.bulkDocs(docs);

  // create a mango index for the 'groups' field.
  db.createIndex({
    index: {
      fields: ['groups'],
      ddoc: "my-index"
    }
  });
  // declare groups_groupIDView map/reduce index for groups.groupID
  const ddoc = {
    _id: '_design/' + groups_groupIDView,
    views: {
      groupIDView: {
        map: function(doc) {
          if (doc.groups instanceof Array) {
            doc.groups.forEach(g => {
              emit(g.groupID);
            })
          }
        }.toString()
      }
    }
  };
  // install the map/reduce design doc
  await db.put(ddoc);

  // candy - gather up and display groupIDs for this demo
  const hints = {};
  docs.forEach(doc => doc.groups.forEach(g => hints[g.groupID] = 1));
  gel(g_groupIdHints).innerText = Object.keys(hints).join(', ');

})();
.hide {
  display: none
}

.label {
  text-align: right;
  margin-right: 1em;
}

.hints {
  font-size: smaller;
}
<script src="https://cdn.jsdelivr.net/npm/pouchdb@7.1.1/dist/pouchdb.min.js"></script>
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.memory.min.js"></script>
<script src="https://github.com/pouchdb/pouchdb/releases/download/7.1.1/pouchdb.find.min.js"></script>

<table>
  <tr>
    <td>
      <button onclick='query(all_docs)'>Show All Docs</button>
    </td>
  </tr>
  <tr>
    <td>
      <label for='groupID'>Find GroupIDs:&nbsp;</label>
      <input type='text' id='groupID' />
      <button onclick='query(groups_groupID,gel("groupID").value)'>Mango</button>
    </td>
  </tr>
  <tr>
    <td>
      <label for='groupIDView'>Find GroupIDs:&nbsp;</label>
      <input type='text' id='groupIDView' />
      <button onclick='query(groups_groupIDView,gel("groupIDView").value)'>Map/Reduce</button>
    </td>
  </tr>
  <tr>
    <td>
      <span class='hints'>Enter CSV of groupID's, e.g. <span id='groupIdHints'></span></span>
    </td>
  </tr>
  <tr>
</table>
<div style='margin-top:2em'></div>
<div>
  <pre id='view_result'></pre>
</div>

1 CouchDB find - Combination Operators

Related