【问题标题】:Pouch DB Fast SearchPouch DB 快速搜索
【发布时间】:2020-10-28 02:23:22
【问题描述】:

我想快速搜索我的邮袋数据库。即使对于包含 1000 个项目和字段索引的小型数据集,我在下面的查询也非常慢。我猜这是因为我使用的是正则表达式。我什至尝试将正则表达式设置为“^”+search(只需搜索开始),这需要一段时间(10 秒)

对以下字段进行 OR 搜索的最佳方法是什么?

代码如下:

        db_items.find({
          selector: {name: {$regex: RegExp("^"+search, "i")}},
          fields: ['_id', 'name','unit_price','category','quantity','item_id'],
          sort: ['name']
        }

我应用了答案中的代码,但仍然存在性能问题。在名称字段上有索引的 10k 文档上花费 20 秒

item_index_creation.push(db_items.createIndex({
  index: {
    fields: ['name']
  }
}));


function item_view_index(doc) 
{
      const regex = /[\s\.;]+/gi;
      ['name'].forEach(field => {
        if (doc[field]) {
          const words = doc[field].replaceAll(regex,',').split(',');
          words.forEach(word => {
            word = word.trim();
            if (word.length) {
              emit(word.toLocaleLowerCase(), [field, word]);
            }
          });
        }
      });
  }

        //This is taking 20+ seconds on 11,000 documents
        const search_results = await db_items.query(item_view_index, {
          include_docs: true,
          reduce: false,
          descending: descending,
          startkey: descending ? search + '\uFFF0' : search,
          endkey: descending ? search : search + '\uFFF0'
        });
        
        var results = search_results.rows;
        var db_response = [];
        for(var k=0;k<results.length;k++)
        {
            var row = results[k].doc;
            var item = {unit_price: to_currency_no_money(row.unit_price), image: default_image,label: row.name+' - '+to_currency_no_money(row.unit_price),category:row.category,quantity: to_quantity(row.quantity),value: row.item_id};
            db_response.push(item);
        }
        response(db_response);

【问题讨论】:

  • 只是为了确定:search 本身就是一个正则表达式,而不仅仅是一个固定的字符串?
  • 我正在使用正则表达式来匹配不区分大小写的单词开头。但可以使用任何其他搜索单词开头的东西。如果需要,我可以将所有文档设为小写。观看以匹配“搜索%”
  • 您是否使用调试来查找有关查询性能的信息? pouchdb.com/guides/mango-queries.html#performance-notes
  • 我看不出这是一个“或”搜索,这个例子比你想要的简单吗?如果您可以小写(或以其他方式规范化)名称并使用 ^,那么这是一个 {$and {$gt} {$lt}},名称上有一个正常的索引。
  • 您的真实数据库中的行数和大小是多少?您是否也愿意在 pouchDB 之上引入新技术以获得最佳结果?

标签: pouchdb


【解决方案1】:

很遗憾,您不会使用$regex 之类的方式获得快速搜索,因为[1]

$regex、$ne 和 $not 不能使用磁盘索引,必须使用 而是在内存中过滤。

就是这样 - 你的情况非常低效。

在你的帖子下的评论中你说

...但是可以使用任何其他搜索单词开头的东西。我会做 如果需要,所有文档都小写。观看以匹配“搜索%”

太棒了!这很容易。但是,如果您绝对必须使用 Mango 查询,这将无法实现 - 满足您的读取要求的最佳选择是使用 map/reduce。

为简单起见,下面的 sn-p 创建了一个 map/reduce 视图,索引两个字段中的任何单词,namecategory。这是map 函数。

function(doc) {
      const regex = /[\s\.;]+/gi;
      ['name', 'category'].forEach(field => {
        if (doc[field]) {
          const words = doc[field].replaceAll(regex,',').split(',');
          words.forEach(word => {
            word = word.trim();
            if (word.length) {
              emit(word.toLocaleLowerCase(), [field, word]);
            }
          });
        }
      });
    }

地图功能的亮点是

  • 多个文档字段来源
  • 所有索引词都是小写的
  • 在索引行value中,保留字段和原词

请注意最后一点,例如您可以进行后期处理以过滤掉某些字段,显示原始单词等。

现在可以查询前缀搜索了。

async function search(term, descending) {
 // snip snip
    term = term.toLocaleLowerCase();
    const result = await db.query(view_index, {
      include_docs: false,
      reduce: false,
      descending: descending,
      startkey: descending ? term + '\uFFF0' : term,
      endkey: descending ? term : term + '\uFFF0'
    });
 // snip snip
}

注意descending 属性;需要颠倒startkeyendkey

运行下面的 sn-p,它使用 Lorem Ipsum 生成器中的单词索引 10k 个文档。它提供了相对较快的响应 - 但请注意此演示使用内存适配器,因此您的里程可能会有所不同,但是我相信通过针对您的特定用例进行一些调整,您应该获得不错的响应时间。祝你好运!

p.s.我建议运行 sn-p 全屏。

const LoremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Amet consectetur adipiscing elit duis tristique. Sed risus ultricies tristique nulla aliquet enim. Pharetra pharetra massa massa ultricies mi. Nisl nisi scelerisque eu ultrices vitae auctor eu. Tellus rutrum tellus pellentesque eu. Justo donec enim diam vulputate ut pharetra sit amet aliquam. Non pulvinar neque laoreet suspendisse interdum consectetur libero. Pellentesque habitant morbi tristique senectus et netus et malesuada. Nisl purus in mollis nunc sed. Ultricies integer quis auctor elit. Ultrices in iaculis nunc sed augue lacus viverra.Eu scelerisque felis imperdiet proin fermentum. Ultrices dui sapien eget mi proin sed libero. Bibendum neque egestas congue quisque egestas diam in arcu cursus. Pretium lectus quam id leo in vitae turpis massa sed. Sed vulputate mi sit amet mauris commodo quis. Elementum eu facilisis sed odio morbi quis commodo odio. Sed viverra ipsum nunc aliquet bibendum enim facilisis gravida neque. Id interdum velit laoreet id donec ultrices. Aenean et tortor at risus viverra adipiscing at. Gravida rutrum quisque non tellus orci ac auctor. Lobortis mattis aliquam faucibus purus in massa tempor nec feugiat. Tempus imperdiet nulla malesuada pellentesque elit eget gravida cum. Justo donec enim diam vulputate. Pharetra vel turpis nunc eget lorem dolor sed viverra. Neque gravida in fermentum et. Vel eros donec ac odio. Egestas egestas fringilla phasellus faucibus.Aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Sit amet cursus sit amet dictum sit amet justo donec.";
const view_index = "view_index";
const max_docs = 10000;

async function search(term, descending) {
  const resultView = gel('results');
  resultView.classList.add('hide');

  term = term.trim();

  if (term.length) {
    let start = Date.now();
    term = term.toLocaleLowerCase();
    const result = await db.query(view_index, {
      include_docs: false,
      reduce: false,
      descending: descending,
      startkey: descending ? term + '\uFFF0' : term,
      endkey: descending ? term : term + '\uFFF0'
    });

    const mark = Date.now() - start;
    const matches = `<div>${uniquify(result.rows).join("<br/>")}</div>`;
    const metrics = `<div>Matches: ${result.rows.length}/${mark}ms</div>`;
    resultView.innerHTML = `<div>${metrics}${matches}</div>`
    resultView.classList.remove('hide');
  }
}
// gather up unique words that matched.  This is sloppy!
function uniquify(rows) {
  let arr = [];
  rows.forEach(row => {
    // note we're displaying the case-sensitive word!
    if (arr.indexOf(row.value[1]) === -1) arr.push(row.value[1]);
  });
  return arr;
}

//
// pouchdb boilerplate code
//
const gel = id => document.getElementById(id);

let db;

// init example db instance
async function initDb() {

  const start = Date.now();

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

  // declare view_index
  const ddoc = {
    _id: '_design/' + view_index,
    views: {}
  };
  ddoc.views[view_index] = {
    map: function(doc) {
      const regex = /[\s\.;]+/gi;
      ['name', 'category'].forEach(field => {
        if (doc[field]) {
          const words = doc[field].replaceAll(regex,
            ',').split(',');
          words.forEach(word => {
            word = word.trim();
            if (word.length) {
              emit(word.toLocaleLowerCase(), [field, word]);
            }
          });
        }
      });
    }.toString()
  };
  // install the map/reduce design doc
  await db.put(ddoc);

  // insert the docs. 
  console.log(`adding ${max_docs} documents...`);
  await db.bulkDocs(getDocsToInstall());

  // force the index to build
  console.log(`Waiting for index '${view_index}' to build...`);
  await db.query(view_index, {
    reduce: true
  });

  console.log(`db inited in ${Date.now()-start}ms`);
}



// canned test documents
function getDocsToInstall() {
  const docs = new Array(max_docs);
  const words = LoremIpsum.split(' ');
  const getWord = () => {
    return words[Math.floor(Math.random() * Math.floor(words.length))];
  }
  for (let i = 0; i < docs.length; i++) {
    docs[i] = {
      name: getWord(),
      category: getWord()
    };
  }
  return docs;
}


initDb().then(() => {
  // setup input listener
  const searchFn = () => {
    search(gel('term').value, gel('sort').checked);
  };
  gel('term').addEventListener('input', searchFn);
  gel('sort').addEventListener('input', searchFn);
  gel('view').classList.remove('hide')
});
.hide {
  display: none
}

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

.hints {
  font-size: smaller;
}
<script src="//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>
<table id='view' class='hide'>
  <tr>
    <td class='label'>
      <label for='term'>Term</label>
    </td>
    <td>
      <input id='term' type='text' />
    </td>
  </tr>
  <tr>
    <td>&nbsp;</td>
    <td class='hints'><em>hint:</em> Lorem, ipsum, dolor, sit, amet, consectetur, adipiscing, elit
    </td>
  </tr>
  <tr>
    <td class='label'>
      <label for='sort'>Sort Desc</label>
    </td>
    <td>
      <input id='sort' type='checkbox' />
    </td>
  </tr>
</table>
<div style='margin-top:2em'></div>
<hr/>
<div id='results' class='hide'>
</div>

1PouchDB Guide - Further reading

【讨论】:

  • 使用内存适配器有什么缺点?
  • @ChrisMuench 内存适配器最适合测试,因为它不是持久的。我将内存适配器用于 SO sn-ps,因此样本不会占用本地存储空间,并且每次运行时它都是“干净的”(无需删除本地数据库)。 FWIW 碰巧我使用 idb 适配器在 jsfiddle 上旋转了代码 sn-p 并且没有看到响应时间有太大差异。
  • 知道了,谢谢。本周将实施此代码,如果我有任何问题,会通知您。但据我所知,它看起来很棒
  • 如何使用函数(文档)?我应该将其重命名为 view_index 吗?它没有名字所以我不知道在哪里使用它
  • 我用答案中的代码更新了我的问题。仍然存在性能问题
【解决方案2】:

我们在 pouchdb 中使用 pouchdb find 和二级索引时遇到了同样的问题。

我们的 pouchdb 有 5K 项,按词搜索真的很慢(在低性能设备上大约需要 20 秒)。

然后我们决定更改搜索以使用 allDocs 函数。我们还利用了文档的 ID。基本上,我们只想对类型为“product”、id 像“product::{someProductId}”的文档进行搜索。所以我们采用了类似以下的方法,它极大地提高了我们的搜索性能(在低性能设备上它下降到 3 秒)

 db.allDocs({
    include_docs: true,
    attachments: false,
    startkey: "product::", // search only in docs starting with product::
    endkey: "product::\ufff0"
  })
  .then(function(result) {
    const rows = result.rows.filter(
      i =>
        i.doc &&
        i.doc.name &&
        (i.doc.name.toLowerCase().indexOf(term.toLowerCase()) > -1 || i.doc. category.toLowerCase().indexOf(term.toLowerCase()) > -1)
    );
    return rows
})

【讨论】:

  • 嗯,这对我不起作用...有没有办法通过常规搜索来做到这一点
【解决方案3】:

我已经构建了一个演示:https://jsbin.com/ralemisufo/1/edit?js,output 请注意,对于较短的代码,我在此答案中省略了来自 sn-ps 的所有错误处理和异步行为。

PouchDB(和 CouchDB)都没有针对任意查询进行优化,当它们可以使用预计算索引时它们会大放异彩。不幸的是,正则表达式总是是一个任意查询,因此不能使用任何索引 (Docs)。您必须以数据库可以帮助您的方式构建数据访问模式。

该演示将 10.000 个文档(在“初始化”时)存储在一个新的数据库中,这些文档由随机词标识。该文档已包含标识名称的小写版本:

for (i=0; i < 10000;i++) {
  var name = randomizedWord();
  docs.push({
    _id: randomizedWord(),
    name: name,
    lname: name.toLowerCase(),
    position: i,
  });
}

(为了高效插入,使用批量 API)。

插入数据时,会为 name 创建一个 Mango 索引。我认为您已经有此代码,因为 PouchDB 在对数据进行排序时会引发错误。之后,为小写名称创建了一个额外的 Mango 索引,这需要几秒钟的时间。请记住,这段时间只用了一次,额外的插入会自动使缓存保持最新:

db.createIndex({
  index: {fields: ['name']}
});
db.createIndex({
  index: {fields: ['lname']}
});

我们现在可以通过一个技巧来查询数据:可以在 PouchDB 和 CouchDB 中模拟前缀匹配,方法是使用前缀 $gte 和使用前缀加上可能的最高字符的 $lt。例如,名称以xa 开头的所有文档的名称都大于或等于xa 且小于xa\uffff (Collation Rules):

db.find({
  selector: {lname: {
    $gte: "xa",
    $lt: "xa\uffff",
  }},
  sort: ["lname"],
})

在我的机器上,“更快”版本通常需要

【讨论】:

    猜你喜欢
    • 2018-10-10
    • 1970-01-01
    • 2015-03-20
    • 2021-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多