袋数据库快速搜索

Pouch DB Fast Search

我想快速搜索我的邮袋数据库。即使对于包含 1000 个项目和字段索引的小型数据集,我下面的查询也非常慢。我猜这是因为我使用的是正则表达式。我什至尝试将正则表达式作为“^”+搜索(只需搜索开头),这需要一段时间(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);

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

我们的 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
})

我已经构建了一个演示:https://jsbin.com/ralemisufo/1/edit?js,output请注意,对于较短的代码,我已经从这个答案的片段中删除了所有错误处理和异步行为。

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

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

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

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

插入数据时,会创建 name 的 Mango 索引。我想您已经有了这方面的代码,否则 PouchDB 在对数据进行排序时会抛出错误。之后,为 lower-cased 名称创建了一个额外的 Mango 索引,这又花费了几秒钟。请记住,这次只花费一次,额外的插入会自动保留缓存 up-to-date:

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

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

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

在我的机器上,“更快”版本通常需要 <100 毫秒,而“问题”版本大约需要 2000 毫秒。当然,这是一个微观基准,不受任何控制,但它可能是未来研究的起点。如果您对 PouchDB 和 CouchDB 索引有其他问题,请告诉我。

不幸的是,您不会使用 $regex 之类的方式进行快速搜索,因为[1]

$regex, $ne, and $not cannot use on-disk indexes, and must use in-memory filtering instead.

就是这样 - 在你的情况下效率很低。

在你 post 下的评论中你说

...But can use anything else that searches start of word. I can make all docs lowercase if needed. Watch to match “search%”

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

为简单起见,下面的代码片段创建了一个 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]);
            }
          });
        }
      });
    }

地图功能的亮点是

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

注意最后一点,例如,您可以 post 过滤掉某些字段、显示原始单词等的过程

现在确定是前缀搜索的查询。

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

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

p.s. 我推荐 运行 全屏片段。

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