DarkMatter in Cyberspace
  • Home
  • Categories
  • Tags
  • Archives

Meteor应用中简单查询的实现方法


简单搜索要求多个检索词能够以任意顺序出现在多个字段中,目前的解决方法有两种思路:

一是创建一个额外的检索项,把所有要检索的字段用字符串方式组合起来放在检索项中, 然后用正则表达式匹配; 这种方式实际上有信息冗余,所以原始数据变化后一定要及时更新检索项;

二是用第三方包实现这种功能。

自定义搜索:检索词放在单独Collection中

优点是检索词单独存放,需要去掉或者更新检索词时, 只要简单地drop掉检索词Collection即可,适于存储大量记录;

缺点有两个: 首先显示查询结果比较麻烦,需要先在检索项集合中搜出命中目标的_id, 然后再查询原始数据表显示完整信息; 其次这种数据结构对组合查询支持不好,比如要查询"名称"字段中含有"abc", 并且"类别"字段中含有"xyz"的展会,由于"类别"字段是一个复杂对象, 需要事先组合成一个字符串,这一项如果放在单独的collection中, 需要查询两个不同的集合,然后取它们的并集。

首先在MongoDB中,创建用于简单搜索的新collection "simpleSearch":

db.fairs.find().forEach(function(elem) {
    var searchStr = elem.chnName + elem.engName + elem.position;
    db.simpleSearch.save({ _id: elem._id, searchBody: searchStr });
})

然后定义路由处理函数:

Fairs = new Mongo.Collection("fairs");
SimpleSearch = new Mongo.Collection("simpleSearch");

Router.route('/results/:inp', function() {
  // basic search url: /results/abc?type=basic
  // complex search url: /results/?type=complex?name=xxx?time=xxx?position=xxx?category=xxx
  var fullStr = this.params.inp;
  var searchType = this.params.query.type;
  if (searchType === "basic") {
    var parts = fullStr.trim().split(" ");
    for (i=0; i<parts.length; ++i) {
      parts[i] = "(?=.*" + parts[i] + ")";
    }
    var queryPtn = new RegExp("(" + parts.join("") + ")", "i");
    var ids = [];
    SimpleSearch.find( { searchBody: queryPtn }, { fields: { searchBody:0 }} ).forEach(
      function(elem) {
        ids.push(elem._id);
      });
    this.render('SearchResults', {
      data: function() {
        return Fairs.find( { _id: { $in: ids } } );
      }
    });
  } else if (searchType === "complex") {
    ...
  }
});

这里首先查询SimpleSearch集合,将查询得到的展会_id存入临时容器ids数组中, 然后在Fairs集合中通过"$in"操作符取得所有ids数组中_id对应的展会对象。

自定义搜索:检索词放在原始信息文档中

这个方法把原始信息和检索项放在同一个集合中,几个检索项放在集合的一个key下面, 集合会比较臃肿,但查询和显示都方便,适用于尺寸不大的数据库。

控制器定义:

Fairs = new Mongo.Collection("fairs");
Router.route('/results/:inp', function() {
  // basic search url: /results/abc?type=basic
  // complex search url: /results/?type=complex?name=xxx?time=xxx?position=xxx?category=xxx
  var fullStr = this.params.inp;
  var searchType = this.params.query.type;
  if (searchType === "basic") {
    var parts = fullStr.trim().split(" ");
    for (i=0; i<parts.length; ++i) {
      parts[i] = "(?=.*" + parts[i] + ")";
    }
    var queryPtn = new RegExp("(" + parts.join("") + ")", "i");
    this.render('SearchResults', {
      data: function() {
        return Fairs.find( { "indexStr.simpleSearch": queryPtn } );
      }
    });
  } else if (searchType === "complex") {
    ...
  }
});

基于Search Source的实现方法

这个搜索基于search-source. 通过meteor add meteorhacks:search-source添加到meteor中, 代码参照了meteor-instant-search-demo.

准备数据源

导入数据

  1. 下载zips.json; 参考Is there a sample MongoDB Database along the lines of world for MySql?.

  2. 导入Meteor数据库中:启动meteor服务,然后在另一个shell里运行: ~/apps/mongodb-linux-x86_64-2.6.5/bin/mongoimport -h localhost:3001 --db meteor --collection zips --type json --file zips.json;

  3. 增加搜索字段:由于简单搜索要求输入可能匹配任意字段, 能达到这个要求的唯一方法就是将被查询的字段连接成一个完整的“搜索”字符串, 输入的各项可以以任何顺序出现在这个搜索字符串(这里这一项名为"forsearch")中, 下面增加这一项:

    db.zips.find().forEach(function(elem) {
        db.zips.update(
          {_id: elem._id},
          { $set: {forsearch: elem.city + ' ' + elem.state}}
        );
    });
    
    db.fairs.find().forEach(function(elem) {
        var cat_minor = _.reduce(elem.category.minor
        db.fairs.update(
          {_id: elem._id},
          { $set: {forsearch: elem.chnName + ' ' + elem.engName + ' ' + elem.time + ' ' + elem.position + catStr}}
        );
    });
    

    参考Update MongoDB field using value of another field.

搜索逻辑

下面"/"都指项目根目录。

数据集定义

在/collections.js中定义数据集和索引:

Zips = new Mongo.Collection('zips');
if(Meteor.isServer) {
  Zips._ensureIndex({city: 1, state: 1});
}

服务端

在/server/server.js中定义:

SearchSource.defineSource('zips', function(searchText, options) {
  var options = {sort: {isoScore: -1}, limit: 20};

  if(searchText) {
    var regExp = buildRegExp(searchText);
    var selector =  {forsearch: regExp};
    return Zips.find(selector, options).fetch();
  } else {
    return Zips.find({}, options).fetch();
  }
});

function buildRegExp(searchText) {
  var parts = searchText.trim().split(/[ \-\:]+/);
  for (i=0; i<parts.length; ++i) {
    parts[i] = "(?=.*" + parts[i] + ")";
  }
  var res = new RegExp("(" + parts.join("") + ")", "i");
  return res;
}

buildRegExp函数中的正则表达式(?=.*A)(?=.*B)可以实现对A和B任意顺序的匹配, 即既能匹配A.B,又能匹配B.A, 参考match string in any word order regex.

客户端:搜索输入框框即时响应

搜索框是通用控件,所以定义在了/client/comTemp.js中,定义了对框中keyup事件的响应:

Template.Header.events({
  "keyup #search-input": _.throttle(function(e) {
    var text = $(e.target).val().trim();
    ZipSearch.search(text);
  }, 200)
});

客户端:搜索结果展示页面

定义在/client/results.html中。

<template name="SearchResults">
  <div id="search-result" class="container content">
    <div id="search-meta">
      {{#if isLoading}}
        searching ...
      {{/if}}
    </div>

    {{#each getZips}}
      <div class="package">
        <h4 class="name">
          {{{city}}}
        </h4>
        <div class="description">
          {{{state}}}, {{{pop}}}
        </div>
      </div>
    {{/each}}
  </div>
</template>

客户端:搜索结果的数据控制

定义在/client/results.js中。

var options = {
  keepHistory: 1000 * 60 * 5,
  localSearch: true
};
var fields = ['forsearch'];     // 这里定义要搜索的字段

ZipSearch = new SearchSource('zips', fields, options);
// 这里的zips要与服务端SearchSource.defineSource中定义的名字一致

Template.SearchResults.helpers({
  getZips: function() {
    return ZipSearch.getData({
      transform: function(matchText, regExp) {
        console.log("matchText: " + matchText);
        console.log("regExp: " + regExp);
        var res = matchText.replace(regExp, "<b>$&</b>");
        console.log("res is: " + res);
        return res;
      },
      sort: {isoScore: -1}
    });
  },

  isLoading: function() {
    return ZipSearch.getStatus().loading;
  }
});

Template.SearchResults.rendered = function() {
  ZipSearch.search('');
};


Published

Jan 30, 2015

Last Updated

Jan 30, 2015

Category

Tech

Tags

  • meteor 47
  • mongodb 24
  • search-source 1
  • simple search 1

Contact

  • Powered by Pelican. Theme: Elegant by Talha Mansoor