Search code examples
pouchdb

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


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
  })
})

Solution

  • 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/[email protected]/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